From 455e53373f7c1dc8390b500883a805d10a974b69 Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 15 Nov 2024 02:49:40 +0800 Subject: [PATCH] 3.0-alpha --- .gitignore | 3 + .gitmodules | 3 + NetEaseMusic-DiscordRPC.sln | 16 ++- Vanessa/AppResource.Designer.cs | 73 +++++++++++ Vanessa/AppResource.resx | 124 +++++++++++++++++++ Vanessa/Configurations.cs | 28 +++++ Vanessa/Constants.cs | 6 + Vanessa/Models/PlayerInfo.cs | 20 +++ Vanessa/MusicPlayer.cs | 10 ++ Vanessa/Players/NetEase.cs | 207 ++++++++++++++++++++++++++++++++ Vanessa/Players/Tencent.cs | 16 +++ Vanessa/Program.cs | 197 ++++++++++++++++++++++++++++++ Vanessa/Resources/icon.ico | Bin 0 -> 4286 bytes Vanessa/Vanessa.csproj | 65 ++++++++++ Vanessa/Win32Api/AutoStart.cs | 71 +++++++++++ Vanessa/Win32Api/Memory.cs | 175 +++++++++++++++++++++++++++ Vanessa/Win32Api/User32.cs | 91 ++++++++++++++ submodule/discord-rpc-csharp | 1 + 18 files changed, 1104 insertions(+), 2 deletions(-) create mode 100644 .gitmodules create mode 100644 Vanessa/AppResource.Designer.cs create mode 100644 Vanessa/AppResource.resx create mode 100644 Vanessa/Configurations.cs create mode 100644 Vanessa/Constants.cs create mode 100644 Vanessa/Models/PlayerInfo.cs create mode 100644 Vanessa/MusicPlayer.cs create mode 100644 Vanessa/Players/NetEase.cs create mode 100644 Vanessa/Players/Tencent.cs create mode 100644 Vanessa/Program.cs create mode 100644 Vanessa/Resources/icon.ico create mode 100644 Vanessa/Vanessa.csproj create mode 100644 Vanessa/Win32Api/AutoStart.cs create mode 100644 Vanessa/Win32Api/Memory.cs create mode 100644 Vanessa/Win32Api/User32.cs create mode 160000 submodule/discord-rpc-csharp diff --git a/.gitignore b/.gitignore index 3e759b7..1e28d5b 100644 --- a/.gitignore +++ b/.gitignore @@ -328,3 +328,6 @@ ASALocalRun/ # MFractors (Xamarin productivity tool) working folder .mfractor/ + + +.publish/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9eae3c0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "submodule/discord-rpc-csharp"] + path = submodule/discord-rpc-csharp + url = https://github.com/Lachee/discord-rpc-csharp diff --git a/NetEaseMusic-DiscordRPC.sln b/NetEaseMusic-DiscordRPC.sln index fc2e37b..6249cac 100644 --- a/NetEaseMusic-DiscordRPC.sln +++ b/NetEaseMusic-DiscordRPC.sln @@ -1,10 +1,14 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27703.2042 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35327.3 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEaseMusic-DiscordRPC", "NetEaseMusic-DiscordRPC\NetEaseMusic-DiscordRPC.csproj", "{8A5639C6-8083-4623-9234-D87878036C4E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vanessa", "Vanessa\Vanessa.csproj", "{5B2859C2-BCDE-46C2-A113-13E48AB0A7B0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordRPC", "submodule\discord-rpc-csharp\DiscordRPC\DiscordRPC.csproj", "{D3E3FE4A-F603-493B-8AE0-A402A91C914F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +19,14 @@ Global {8A5639C6-8083-4623-9234-D87878036C4E}.Debug|Any CPU.Build.0 = Debug|Any CPU {8A5639C6-8083-4623-9234-D87878036C4E}.Release|Any CPU.ActiveCfg = Release|Any CPU {8A5639C6-8083-4623-9234-D87878036C4E}.Release|Any CPU.Build.0 = Release|Any CPU + {5B2859C2-BCDE-46C2-A113-13E48AB0A7B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B2859C2-BCDE-46C2-A113-13E48AB0A7B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B2859C2-BCDE-46C2-A113-13E48AB0A7B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B2859C2-BCDE-46C2-A113-13E48AB0A7B0}.Release|Any CPU.Build.0 = Release|Any CPU + {D3E3FE4A-F603-493B-8AE0-A402A91C914F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3E3FE4A-F603-493B-8AE0-A402A91C914F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3E3FE4A-F603-493B-8AE0-A402A91C914F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3E3FE4A-F603-493B-8AE0-A402A91C914F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Vanessa/AppResource.Designer.cs b/Vanessa/AppResource.Designer.cs new file mode 100644 index 0000000..060303e --- /dev/null +++ b/Vanessa/AppResource.Designer.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// +// 此代码由工具生成。 +// 运行时版本:4.0.30319.42000 +// +// 对此文件的更改可能会导致不正确的行为,并且如果 +// 重新生成代码,这些更改将会丢失。 +// +//------------------------------------------------------------------------------ + +namespace Kxnrl.Vanessa { + using System; + + + /// + /// 一个强类型的资源类,用于查找本地化的字符串等。 + /// + // 此类是由 StronglyTypedResourceBuilder + // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 + // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen + // (以 /str 作为命令选项),或重新生成 VS 项目。 + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class AppResource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal AppResource() { + } + + /// + /// 返回此类使用的缓存的 ResourceManager 实例。 + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Kxnrl.Vanessa.AppResource", typeof(AppResource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// 重写当前线程的 CurrentUICulture 属性,对 + /// 使用此强类型资源类的所有资源查找执行重写。 + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// 查找类似于 (图标) 的 System.Drawing.Icon 类型的本地化资源。 + /// + internal static System.Drawing.Icon icon { + get { + object obj = ResourceManager.GetObject("icon", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + } +} diff --git a/Vanessa/AppResource.resx b/Vanessa/AppResource.resx new file mode 100644 index 0000000..8bc9279 --- /dev/null +++ b/Vanessa/AppResource.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + Resources\icon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/Vanessa/Configurations.cs b/Vanessa/Configurations.cs new file mode 100644 index 0000000..8034855 --- /dev/null +++ b/Vanessa/Configurations.cs @@ -0,0 +1,28 @@ +using System; +using System.IO; +using System.Text; + +namespace Kxnrl.Vanessa; + +internal class Configurations +{ + public bool IsFirstLoad { get; private set; } + + private readonly string _path; + + public Configurations() + { + var dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Vanessa"); + Directory.CreateDirectory(dir); + + _path = Path.Combine(dir, "config.json"); + + IsFirstLoad = File.Exists(_path); + + File.WriteAllText(_path, "{}", Encoding.UTF8); + } + + public void Save() + { + } +} \ No newline at end of file diff --git a/Vanessa/Constants.cs b/Vanessa/Constants.cs new file mode 100644 index 0000000..2d90c13 --- /dev/null +++ b/Vanessa/Constants.cs @@ -0,0 +1,6 @@ +namespace Kxnrl.Vanessa; + +internal static class Constants +{ + public static Configurations GlobalConfig = new (); +} diff --git a/Vanessa/Models/PlayerInfo.cs b/Vanessa/Models/PlayerInfo.cs new file mode 100644 index 0000000..f1aa652 --- /dev/null +++ b/Vanessa/Models/PlayerInfo.cs @@ -0,0 +1,20 @@ +using System; + +namespace Kxnrl.Vanessa.Models; + +internal readonly record struct PlayerInfo +{ + public required string Identity { get; init; } + public required string Title { get; init; } + public required string Artists { get; init; } + public required string Album { get; init; } + public required string Cover { get; init; } + public required double Schedule { get; init; } + public required double Duration { get; init; } + public required string Url { get; init; } + + public required bool Pause { get; init; } + + public override int GetHashCode() + => HashCode.Combine(Identity, Pause); +} diff --git a/Vanessa/MusicPlayer.cs b/Vanessa/MusicPlayer.cs new file mode 100644 index 0000000..9c75a7d --- /dev/null +++ b/Vanessa/MusicPlayer.cs @@ -0,0 +1,10 @@ +using Kxnrl.Vanessa.Models; + +namespace Kxnrl.Vanessa; + +internal interface IMusicPlayer +{ + bool Validate(int pid); + + PlayerInfo? GetPlayerInfo(); +} diff --git a/Vanessa/Players/NetEase.cs b/Vanessa/Players/NetEase.cs new file mode 100644 index 0000000..6705284 --- /dev/null +++ b/Vanessa/Players/NetEase.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Kxnrl.Vanessa.Models; +using Kxnrl.Vanessa.Win32Api; + +namespace Kxnrl.Vanessa.Players; + +internal sealed class NetEase : IMusicPlayer +{ + private readonly string _path; + + private readonly int _pid; + private readonly ProcessMemory _process; + private readonly nint _audioPlayerPointer; + private readonly nint _schedulePointer; + + private const string AudioPlayerPattern + = "48 8D 0D ? ? ? ? E8 ? ? ? ? 48 8D 0D ? ? ? ? E8 ? ? ? ? 90 48 8D 0D ? ? ? ? E8 ? ? ? ? 48 8D 05 ? ? ? ? 48 8D A5 ? ? ? ? 5F 5D C3 CC CC CC CC CC 48 89 4C 24 ? 55 57 48 81 EC ? ? ? ? 48 8D 6C 24 ? 48 8D 7C 24"; + + private const string AudioSchedulePattern = "66 0F 2E 0D ? ? ? ? 7A ? 75 ? 66 0F 2E 15"; + + public NetEase(int pid) + { + _pid = pid; + + _path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "NetEase", + "CloudMusic", + "WebData", + "file", + "playingList"); + + using var p = Process.GetProcessById(pid); + + foreach (ProcessModule module in p.Modules) + { + if (!"cloudmusic.dll".Equals(module.ModuleName)) + { + continue; + } + + var process = new ProcessMemory(pid); + var address = module.BaseAddress; + + if (Memory.FindPattern(AudioPlayerPattern, pid, address, out var app)) + { + var textAddress = nint.Add(app, 3); + var displacement = process.ReadInt32(textAddress); + + _audioPlayerPointer = textAddress + displacement + sizeof(int); + } + + if (Memory.FindPattern(AudioSchedulePattern, pid, address, out var asp)) + { + var textAddress = nint.Add(asp, 4); + var displacement = process.ReadInt32(textAddress); + _schedulePointer = textAddress + displacement + sizeof(int); + } + + _process = process; + + break; + } + + if (_audioPlayerPointer == nint.Zero) + { + throw new EntryPointNotFoundException("Failed to find AudioPlayer"); + } + + if (_schedulePointer == nint.Zero) + { + throw new EntryPointNotFoundException("Failed to find Scheduler"); + } + + if (_process is null) + { + throw new EntryPointNotFoundException("Failed to find process"); + } + } + + public bool Validate(int pid) + => pid == _pid; + + public PlayerInfo? GetPlayerInfo() + { + var jsonData = File.Exists(_path) ? File.ReadAllText(_path) : null; + + var playlist = jsonData is { } json ? JsonSerializer.Deserialize(json) : null; + + if (playlist is null || playlist.List.Count == 0) + { + return null; + } + + var status = GetPlayerStatus(); + + if (status == PlayStatus.Waiting) + { + return null; + } + + var identity = GetCurrentSongId(); + + if (playlist.List.Find(x => x.Identity == identity) is not { Track: { } track }) + { + return null; + } + + return new PlayerInfo + { + Identity = identity, + Title = track.Name, + Artists = string.Join(',', track.Artists.Select(x => x.Singer)), + Album = track.Album.Name, + Cover = track.Album.Cover, + Duration = GetSongDuration(), + Schedule = GetSchedule(), + Pause = status == PlayStatus.Paused, + + // lock + Url = $"https://music.163.com/#/song?id={identity}", + }; + } + +#region Unsafe + + private enum PlayStatus + { + Waiting, + Playing, + Paused, + Unknown3, + Unknown4, + } + + private double GetSchedule() + => _process.ReadDouble(_schedulePointer); + + private PlayStatus GetPlayerStatus() + => (PlayStatus) _process.ReadInt32(_audioPlayerPointer, 0x60); + + private float GetPlayerVolume() + => _process.ReadFloat(_audioPlayerPointer, 0x64); + + private float GetCurrentVolume() + => _process.ReadFloat(_audioPlayerPointer, 0x68); + + private double GetSongDuration() + => _process.ReadDouble(_audioPlayerPointer, 0xa8); + + private string GetCurrentSongId() + { + var audioPlayInfo = _process.ReadInt64(_audioPlayerPointer, 0x50); + + if (audioPlayInfo == 0) + { + return string.Empty; + } + + var strPtr = audioPlayInfo + 0x10; + + var strLength = _process.ReadInt64((nint) strPtr, 0x10); + + // small string optimization + byte[] strBuffer; + + if (strLength <= 15) + { + strBuffer = _process.ReadBytes((nint) strPtr, (int) strLength); + } + else + { + var strAddress = _process.ReadInt64((nint) strPtr); + strBuffer = _process.ReadBytes((nint) strAddress, (int) strLength); + } + + var str = Encoding.UTF8.GetString(strBuffer); + + return string.IsNullOrEmpty(str) ? string.Empty : str[..str.IndexOf('_')]; + } + +#endregion +} + +file record NetEasePlaylistTrackArtist([property: JsonPropertyName("name")] string Singer); + +file record NetEasePlaylistTrackAlbum( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("cover")] string Cover); + +file record NetEasePlaylistTrack( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("artists")] + NetEasePlaylistTrackArtist[] Artists, + [property: JsonPropertyName("album")] NetEasePlaylistTrackAlbum Album); + +file record NetEasePlaylistItem( + [property: JsonPropertyName("id")] string Identity, + [property: JsonPropertyName("track")] NetEasePlaylistTrack Track); + +file record NetEasePlaylist([property: JsonPropertyName("list")] List List); diff --git a/Vanessa/Players/Tencent.cs b/Vanessa/Players/Tencent.cs new file mode 100644 index 0000000..6a5f3ee --- /dev/null +++ b/Vanessa/Players/Tencent.cs @@ -0,0 +1,16 @@ +using System; +using Kxnrl.Vanessa.Models; + +namespace Kxnrl.Vanessa.Players; + +internal sealed class Tencent : IMusicPlayer +{ + public Tencent(int pid) + => throw new NotImplementedException(); + + public bool Validate(int pid) + => throw new System.NotImplementedException(); + + public PlayerInfo? GetPlayerInfo() + => throw new System.NotImplementedException(); +} diff --git a/Vanessa/Program.cs b/Vanessa/Program.cs new file mode 100644 index 0000000..71306cd --- /dev/null +++ b/Vanessa/Program.cs @@ -0,0 +1,197 @@ +using System; +using System.Diagnostics; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using DiscordRPC; +using Kxnrl.Vanessa.Players; +using Button = DiscordRPC.Button; + +namespace Kxnrl.Vanessa; + +internal class Program +{ + private const string NetEaseAppId = "481562643958595594"; + private const string TencentAppId = "903485504899665990"; + + private static async Task Main() + { + // check run once + _ = new Mutex(true, "MusicDiscordRpc", out var allow); + + if (!allow) + { + MessageBox.Show("MusicDiscordRpc is already running.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + + Environment.Exit(-1); + + return; + } + + if (Constants.GlobalConfig.IsFirstLoad) + { + Win32Api.AutoStart.Set(true); + } + + var netEase = new DiscordRpcClient(NetEaseAppId); + var tencent = new DiscordRpcClient(TencentAppId); + netEase.Initialize(); + tencent.Initialize(); + + if (!netEase.IsInitialized || !tencent.IsInitialized) + { + MessageBox.Show("Failed to init rpc client.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + Environment.Exit(-1); + } + + // TODO Online Signatures + await Task.CompletedTask; + + var notifyMenu = new ContextMenuStrip(); + + var exitButton = new ToolStripMenuItem("Exit"); + var autoButton = new ToolStripMenuItem("AutoStart" + " " + (Win32Api.AutoStart.Check() ? "√" : "✘")); + notifyMenu.Items.Add(autoButton); + notifyMenu.Items.Add(exitButton); + + var notifyIcon = new NotifyIcon() + { + BalloonTipIcon = ToolTipIcon.Info, + ContextMenuStrip = notifyMenu, + Text = "NetEase Cloud Music DiscordRPC", + Icon = AppResource.icon, + Visible = true, + }; + + exitButton.Click += (_, _) => + { + notifyIcon.Visible = false; + Thread.Sleep(100); + Environment.Exit(0); + }; + + autoButton.Click += (_, _) => + { + var x = Win32Api.AutoStart.Check(); + + Win32Api.AutoStart.Set(!x); + + autoButton.Text = "AutoStart" + " " + (Win32Api.AutoStart.Check() ? "√" : "✘"); + }; + + _ = Task.Run(async () => await UpdateThread(netEase, tencent)); + Application.Run(); + } + + private static async Task UpdateThread(DiscordRpcClient netEase, DiscordRpcClient tencent) + { + IMusicPlayer? lastInstance = null; + DiscordRpcClient? lastRpcClient = null; + + while (true) + { + try + { + IMusicPlayer player; + DiscordRpcClient rpcClient; + + if (Win32Api.User32.GetWindowTitle("OrpheusBrowserHost", out _, out var netEaseProcessId)) + { + player = lastInstance is null + ? new NetEase(netEaseProcessId) + : lastInstance.Validate(netEaseProcessId) + ? lastInstance + : new NetEase(netEaseProcessId); + + rpcClient = netEase; + } + else if (Win32Api.User32.GetWindowTitle("QQMusic_Daemon_Wnd", out _, out var tencentId)) + { + player = lastInstance is null + ? new Tencent(tencentId) + : lastInstance.Validate(netEaseProcessId) + ? lastInstance + : new Tencent(tencentId); + + rpcClient = tencent; + } + else + { + lastInstance = null; + + continue; + } + + lastInstance = player; + + var pi = player.GetPlayerInfo(); + + Debug.Print(pi is not null ? JsonSerializer.Serialize(pi) : "null"); + + if (pi is not { } info) + { + lastRpcClient?.ClearPresence(); + lastRpcClient = null; + + continue; + } + + if (info.Pause) + { + rpcClient.ClearPresence(); + } + else + { + rpcClient.Update(rpc => + { + rpc.Details = $"🎵 {info.Title}"; + rpc.State = $"🎤 {info.Artists}"; + rpc.Type = ActivityType.Listening; + + rpc.Timestamps = new Timestamps(DateTime.UtcNow.Subtract(TimeSpan.FromSeconds(info.Schedule)), + DateTime.UtcNow.Subtract(TimeSpan.FromSeconds(info.Schedule)) + .Add(TimeSpan.FromSeconds(info.Duration))); + + rpc.Assets = new Assets + { + LargeImageKey = info.Cover, + LargeImageText = info.Album, + SmallImageKey = "timg", + SmallImageText = "NetEase CloudMusic", + }; + + rpc.Buttons = + [ + new Button + { + Label = "🎧 Listen", + Url = info.Url, + }, + new Button + { + Label = "👏 View App on GitHub", + Url = "https://github.com/Kxnrl/NetEase-Cloud-Music-DiscordRPC", + }, + ]; + }); + } + + lastRpcClient = rpcClient; + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + // 用户就喜欢超低内存占用 + // 但是实际上来说并没有什么卵用 + GC.Collect(); + GC.WaitForFullGCComplete(); + + await Task.Delay(TimeSpan.FromMilliseconds(233)); + } + } + } +} diff --git a/Vanessa/Resources/icon.ico b/Vanessa/Resources/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..361964de3e35a4bd0c6035c5fccf801d9b8e0f45 GIT binary patch literal 4286 zcmc(j{cDz07{~9qO-)W4&UJ1~k|zGH zUY+RQ*#j4%x2gu%x&kgibp-4jp`J03-Qf$Roe^J7p`D0xzvt1YbO3AsTslMTe$3I z-*QBp^Ss?L4{>jDRdYXa9(2}DxYUg1GRZoIjJN`lnyX0AS#yQwDR}Z7q7` zUGb?lP55{je%J9g>holMuV3bT-hju!+*Wf`ds^_h2HQbzU$lMb6idugj6dYBYHQfx zi=Hdl&#_Z|#Z7#vC;2n;^{rF2}r^gQX&$C~>Dtz_8I$}GI|55VLkN-vJ0{_e` zhFibKSN$rclJA)V-b3iG`h7)v7`^z$xZ@i0zZF}}+6n6T>a_SH;)53@w~)g;fm(bqeF>OY z&r9rA&W4zEDSFjR{Wgc3t|zD8gQYwuKAnAGbN2Wh&Zl&$<2CB|K0T<|;#{nU9d%&_ zSJM6L{RJPxa5%=^mGIY{yqM=^)=px48y>>bJ+D=wZ;|jFQZOgrx}AOMA=P$f%_HXY zwH@D@!;8p)@~k*CSB^2aHs-a^?39Sl8hY|5?{!aWE}Wvy`@nq++!D@_-AYQ^`CzEd*)Eh?Nwup^%~ z_J#W(GaK-G9RHg6L(G1iu`|TD*~gaIE_hvp-$Z`*kP~tzJe>jESK)gp`fhX;UwX!W zw=JiK-T_13o65zntlxp7{AWCe`>JRUQlC~|qs%PzcX!o3gCEtuQQVn!%7bRL`o0Gq z?-Fl6cf(e6inY_{BldsM9&zQu0cvs_o|DYPn{eFZZ9`pW`fud7dPkyMhPtZ$zDAle z%)t~K^@zT+jGy1iiN2N3VfP?=l>_-`1n+h>%7sL=x#g=T)%@R{_o`~7Gjj>-Ltt;f zR`;Ox-Olen(`TLwv)-(L$0M$q_VT=u-cj9jPX0aYTQLbkc$$+E#jLpg3+$Lb)kJuj OwT + + + WinExe + net9.0-windows + disable + 12 + enable + MusicRpc + Kxnrl.Vanessa + 1.0 + 1 + $(VersionPrefix).$(VersionSuffix) + https://github.com/Kxnrl/NetEase-Cloud-Music-DiscordRPC + ©2024 Kyle + https://github.com/Kxnrl/NetEase-Cloud-Music-DiscordRPC + x64 + true + True + Resources\icon.ico + Kyle + Music Discord Rpc + Music Discord Rpc + + + + embedded + True + True + + + + embedded + True + True + + + + + + + + + + + + + + + + + True + True + AppResource.resx + + + + + + ResXFileCodeGenerator + AppResource.Designer.cs + + + + diff --git a/Vanessa/Win32Api/AutoStart.cs b/Vanessa/Win32Api/AutoStart.cs new file mode 100644 index 0000000..16288a9 --- /dev/null +++ b/Vanessa/Win32Api/AutoStart.cs @@ -0,0 +1,71 @@ +using System; +using Microsoft.Win32; + +namespace Kxnrl.Vanessa.Win32Api; + +internal static class AutoStart +{ + internal static void Set(bool enable) + { + try + { + // Open Base Key. + using var baseKey = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64) + .OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true); + + if (baseKey == null) + { + // wtf? + Console.WriteLine(@"Cannot find Software\Microsoft\Windows\CurrentVersion\Run"); + + return; + } + + if (enable) + + { + baseKey.SetValue("NCM-DiscordRpc", $"{AppContext.BaseDirectory}MusicRpc.exe"); + Console.WriteLine("AutoStartup has been set."); + } + else + { + baseKey.DeleteValue("NCM-DiscordRpc", false); + Console.WriteLine("AutoStartup has been deleted."); + } + } + catch (Exception e) + { + Console.WriteLine($"Failed to set autostartup: {e.Message}"); + } + } + + public static bool Check() + { + try + { + // Open Base Key. + using var baseKey = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64) + .OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true); + + if (baseKey == null) + { + // wtf? + Console.WriteLine(@"Cannot find Software\Microsoft\Windows\CurrentVersion\Run"); + + return false; + } + + var ace = baseKey.GetValue("NCM-DiscordRpc"); + + var exe = $"{AppContext.BaseDirectory}MusicRpc.exe"; + + return exe.Equals(ace); + } + catch (Exception e) + { + Console.WriteLine($"Failed to set autostartup: {e.Message}"); + } + + return false; + } +} diff --git a/Vanessa/Win32Api/Memory.cs b/Vanessa/Win32Api/Memory.cs new file mode 100644 index 0000000..babde9f --- /dev/null +++ b/Vanessa/Win32Api/Memory.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Kxnrl.Vanessa.Win32Api; + +internal static class Memory +{ + public static bool FindPattern(string pattern, int processId, nint address, out nint pointer) + { + var memory = new ProcessMemory(processId); + + var ntOffset = memory.ReadInt32(address, 0x3C); + var ntHeader = address + ntOffset; + + // IMAGE + var fileHeader = ntHeader + 4; + var sections = memory.ReadInt16(ntHeader, 6); + + // OPT HEADER + var optHeader = fileHeader + 20; + var sectionHeader = optHeader + 240; + + var cursor = sectionHeader; + + var pStart = nint.Zero; + var memoryBlock = new List(); + + for (var i = 0; i < sections; i++) + { + var name = memory.ReadInt64(cursor); + + if (name == 0x747865742E) + { + var offset = memory.ReadInt32(cursor, 12); + + pStart = address + offset; + + var size = memory.ReadInt32(cursor, 8); + var buffer = memory.ReadBytes(pStart, size); + memoryBlock.AddRange(buffer); + + break; + } + + cursor += 40; + } + + pointer = FindPattern(pattern, pStart, memoryBlock); + + return pointer != nint.Zero; + } + + private static nint FindPattern(string pattern, nint pStart, List memoryBlock) + { + if (pattern.Length == 0 || pStart == nint.Zero || memoryBlock.Count == 0) + { + return nint.Zero; + } + + var bytes = ParseSignature(pattern); + var first = bytes[0]; + var result = nint.Zero; + + var range = memoryBlock.Count - bytes.Length; + + for (var i = 0; i < range; i++) + { + if (first != 0xFFFF) + { + i = memoryBlock.IndexOf((byte) first, i); + + if (i == -1) + { + break; + } + } + + var found = true; + + for (var j = 1; j < bytes.Length; j++) + { + var wildcard = bytes[j] == 0xFFFF; + var equals = bytes[j] == memoryBlock[i + j]; + + if (wildcard || equals) + { + continue; + } + + found = false; + + break; + } + + if (!found) + { + continue; + } + + result = nint.Add(pStart, i); + + break; + } + + return result; + } + + private static ushort[] ParseSignature(string signature) + { + var bytesStr = signature.Split(' ') + .AsSpan(); + + var bytes = new ushort[bytesStr.Length]; + + for (var i = 0; i < bytes.Length; i++) + { + var str = bytesStr[i]; + + if (str.Contains('?')) + { + bytes[i] = 0xFFFF; + + continue; + } + + bytes[i] = Convert.ToByte(str, 16); + } + + return bytes; + } +} + +internal sealed class ProcessMemory +{ + private readonly nint _process; + + public ProcessMemory(nint process) + => _process = process; + + public ProcessMemory(int processId) + => _process = OpenProcess(0x0010, IntPtr.Zero, processId); + + public byte[] ReadBytes(IntPtr offset, int length) + { + var bytes = new byte[length]; + ReadProcessMemory(_process, offset, bytes, length, IntPtr.Zero); + + return bytes; + } + + public float ReadFloat(IntPtr address, int offset = 0) + => BitConverter.ToSingle(ReadBytes(IntPtr.Add(address, offset), 4), 0); + + public double ReadDouble(IntPtr address, int offset = 0) + => BitConverter.ToDouble(ReadBytes(IntPtr.Add(address, offset), 8), 0); + + public long ReadInt64(IntPtr address, int offset = 0) + => BitConverter.ToInt64(ReadBytes(IntPtr.Add(address, offset), 8), 0); + + public ulong ReadUInt64(IntPtr address, int offset = 0) + => BitConverter.ToUInt64(ReadBytes(IntPtr.Add(address, offset), 8), 0); + + public short ReadInt16(IntPtr address, int offset = 0) + => BitConverter.ToInt16(ReadBytes(IntPtr.Add(address, offset), 2), 0); + + public int ReadInt32(IntPtr address, int offset = 0) + => BitConverter.ToInt32(ReadBytes(IntPtr.Add(address, offset), 4), 0); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool ReadProcessMemory(IntPtr pHandle, IntPtr address, byte[] buffer, int size, IntPtr bytesRead); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr OpenProcess(int dwDesiredAccess, IntPtr bInheritHandle, int dwProcessId); +} diff --git a/Vanessa/Win32Api/User32.cs b/Vanessa/Win32Api/User32.cs new file mode 100644 index 0000000..64cbd26 --- /dev/null +++ b/Vanessa/Win32Api/User32.cs @@ -0,0 +1,91 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Kxnrl.Vanessa.Win32Api; + +internal static class User32 +{ + [StructLayout(LayoutKind.Sequential)] + private struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + [DllImport("user32.dll")] + private static extern IntPtr GetForegroundWindow(); + + [DllImport("user32.dll")] + private static extern IntPtr GetDesktopWindow(); + + [DllImport("user32.dll")] + private static extern IntPtr GetShellWindow(); + + [DllImport("user32.dll", SetLastError = true)] + private static extern int GetWindowRect(IntPtr hwnd, out RECT rc); + + [DllImport("user32.dll", EntryPoint = "FindWindow")] + private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + + private delegate bool EnumWindowsProc(IntPtr hWnd, int lParam); + + [DllImport("user32.dll")] + private static extern bool EnumWindows(EnumWindowsProc enumProc, IntPtr lParam); + + [DllImport("user32.dll")] + private static extern void GetClassName(IntPtr hwnd, StringBuilder sb, int nMaxCount); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern int GetWindowText(IntPtr hWnd, StringBuilder strText, int maxCount); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern int GetWindowTextLength(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern int GetWindowThreadProcessId(IntPtr handle, out int pid); + + private static string GetClassName(IntPtr hwnd) + { + var sb = new StringBuilder(256); + GetClassName(hwnd, sb, 256); + + return sb.ToString(); + } + + private static string GetWindowTitle(IntPtr hwnd) + { + var length = GetWindowTextLength(hwnd); + var sb = new StringBuilder(256); + GetWindowText(hwnd, sb, length + 1); + + return sb.ToString(); + } + + public static bool GetWindowTitle(string match, out string text, out int pid) + { + var title = string.Empty; + var processId = 0; + + EnumWindows(delegate(IntPtr handle, int param) + { + var classname = GetClassName(handle); + + if (match.Equals(classname) && GetWindowThreadProcessId(handle, out var xpid) != 0 && xpid != 0) + { + title = GetWindowTitle(handle); + processId = xpid; + } + + return true; + }, + IntPtr.Zero); + + text = title; + pid = processId; + + return !string.IsNullOrEmpty(title) && pid > 0; + } +} diff --git a/submodule/discord-rpc-csharp b/submodule/discord-rpc-csharp new file mode 160000 index 0000000..7669993 --- /dev/null +++ b/submodule/discord-rpc-csharp @@ -0,0 +1 @@ +Subproject commit 76699932607f3e6470be45823184bdaa84382951