├── .gitignore ├── BeatmapInfo └── Beatmap.cs ├── DefaultLanguage.cs ├── Helper ├── HardwareInformationHelper.cs ├── OsuStatusHelper.cs └── PathHelper.cs ├── LICENSE ├── Language ├── en-US │ └── OsuRTDataProvider.DefaultLanguage.lang ├── ja-JP │ └── OsuRTDataProvider.DefaultLanguage.lang └── zh-CN │ └── OsuRTDataProvider.DefaultLanguage.lang ├── Listen ├── ErrorStatisticsResult.cs ├── HitEvent.cs ├── OSUListenerManager.cs ├── PlayMode.cs └── ProvideData.cs ├── Memory ├── BeatmapOffsetInfo.cs ├── LogHelper.cs ├── OsuBeatmapFinder.cs ├── OsuFinderBase.cs ├── OsuHitEventFinder.cs ├── OsuInternalStatus.cs ├── OsuModeFinder.cs ├── OsuPlayFinder.cs ├── OsuStatusFinder.cs ├── OsuVersionCompareInfoAttribute.cs └── SigScan.cs ├── Mods └── ModsInfo.cs ├── OsuRTDataProvider.csproj ├── OsuRTDataProviderPlugin.cs ├── Properties └── AssemblyInfo.cs ├── README-CN.md ├── README.md ├── Setting.cs ├── UpdateChecker.cs └── Utils.cs /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | 11 | [Dd]ebug/ 12 | [Rr]elease/ 13 | x64/ 14 | build/ 15 | [Bb]in/ 16 | [Oo]bj/ 17 | 18 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 19 | !packages/*/build/ 20 | 21 | # MSTest test Results 22 | [Tt]est[Rr]esult*/ 23 | [Bb]uild[Ll]og.* 24 | 25 | *_i.c 26 | *_p.c 27 | *.ilk 28 | *.meta 29 | *.obj 30 | *.pch 31 | *.pdb 32 | *.pgc 33 | *.pgd 34 | *.rsp 35 | *.sbr 36 | *.tlb 37 | *.tli 38 | *.tlh 39 | *.tmp 40 | *.tmp_proj 41 | *.log 42 | *.vspscc 43 | *.vssscc 44 | .builds 45 | *.pidb 46 | *.log 47 | *.scc 48 | *.json 49 | 50 | # Visual C++ cache files 51 | ipch/ 52 | *.aps 53 | *.ncb 54 | *.opensdf 55 | *.sdf 56 | *.cachefile 57 | 58 | # Visual Studio profiler 59 | *.psess 60 | *.vsp 61 | *.vspx 62 | 63 | # Guidance Automation Toolkit 64 | *.gpState 65 | 66 | # ReSharper is a .NET coding add-in 67 | _ReSharper*/ 68 | *.[Rr]e[Ss]harper 69 | 70 | # TeamCity is a build add-in 71 | _TeamCity* 72 | 73 | # DotCover is a Code Coverage Tool 74 | *.dotCover 75 | 76 | # NCrunch 77 | *.ncrunch* 78 | .*crunch*.local.xml 79 | 80 | # Installshield output folder 81 | [Ee]xpress/ 82 | 83 | # DocProject is a documentation generator add-in 84 | DocProject/buildhelp/ 85 | DocProject/Help/*.HxT 86 | DocProject/Help/*.HxC 87 | DocProject/Help/*.hhc 88 | DocProject/Help/*.hhk 89 | DocProject/Help/*.hhp 90 | DocProject/Help/Html2 91 | DocProject/Help/html 92 | 93 | # Click-Once directory 94 | publish/ 95 | 96 | # Publish Web Output 97 | *.Publish.xml 98 | *.pubxml 99 | 100 | # NuGet Packages Directory 101 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 102 | #packages/ 103 | 104 | # Windows Azure Build Output 105 | csx 106 | *.build.csdef 107 | 108 | # Windows Store app package directory 109 | AppPackages/ 110 | 111 | # Others 112 | sql/ 113 | *.Cache 114 | ClientBin/ 115 | [Ss]tyle[Cc]op.* 116 | ~$* 117 | *~ 118 | *.dbmdl 119 | *.[Pp]ublish.xml 120 | *.pfx 121 | *.publishsettings 122 | 123 | # RIA/Silverlight projects 124 | Generated_Code/ 125 | 126 | # Backup & report files from converting an old project file to a newer 127 | # Visual Studio version. Backup files are not needed, because we have git ;-) 128 | _UpgradeReport_Files/ 129 | Backup*/ 130 | UpgradeLog*.XML 131 | UpgradeLog*.htm 132 | 133 | # SQL Server files 134 | App_Data/*.mdf 135 | App_Data/*.ldf 136 | 137 | # ========================= 138 | # Windows detritus 139 | # ========================= 140 | 141 | # Windows image file caches 142 | Thumbs.db 143 | ehthumbs.db 144 | 145 | # Folder config file 146 | Desktop.ini 147 | 148 | # Recycle Bin used on file shares 149 | $RECYCLE.BIN/ 150 | 151 | # Mac crap 152 | .DS_Store 153 | 154 | .vs/ 155 | .vscode/ 156 | 157 | OtherPlugins/MemoryReader/ 158 | /.idea/.idea.OsuRTDataProvider.dir/.idea 159 | /OsuRTDataProvider.sln 160 | /OsuRTDataProviderVisualStudio.sln 161 | /ObseletedBeatmapFinderVersionDetector.cs 162 | /BranchBackup.txt 163 | -------------------------------------------------------------------------------- /BeatmapInfo/Beatmap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using static OsuRTDataProvider.DefaultLanguage; 5 | 6 | namespace OsuRTDataProvider.BeatmapInfo 7 | { 8 | public class Beatmap 9 | { 10 | public int BeatmapID { get; private set; } 11 | 12 | public string DownloadLink 13 | { 14 | get 15 | { 16 | if (BeatmapID != 0) return $"http://osu.ppy.sh/b/{BeatmapID}"; 17 | return LANG_BEATMAP_NOT_FOUND; 18 | } 19 | } 20 | 21 | /// 22 | /// If BeatmapSetID > 0. Return beatmap's download link. 23 | /// 24 | public string DownloadLinkSet 25 | { 26 | get 27 | { 28 | if (BeatmapSetID > 0) return $"http://osu.ppy.sh/s/{BeatmapSetID}"; 29 | return LANG_BEATMAP_NOT_FOUND; 30 | } 31 | } 32 | 33 | private int m_beatmap_set_id = -1; 34 | 35 | /// 36 | /// Return set id. 37 | /// If no found return -1; 38 | /// 39 | public int BeatmapSetID 40 | { 41 | get 42 | { 43 | if (m_beatmap_set_id > 0) return m_beatmap_set_id; 44 | 45 | if (Folder.Length > 0) 46 | { 47 | string name = Folder; 48 | int len = name.IndexOf(' '); 49 | if (len != -1) 50 | { 51 | string id = name.Substring(0, len); 52 | 53 | if (int.TryParse(id, out m_beatmap_set_id)) 54 | return m_beatmap_set_id; 55 | } 56 | } 57 | return -1; 58 | } 59 | private set => m_beatmap_set_id = value; 60 | } 61 | 62 | public string Version { get; private set; } = string.Empty; 63 | public string Difficulty => Version; 64 | public string Creator { get; private set; } = string.Empty; 65 | public string Artist { get; private set; } = string.Empty; 66 | public string ArtistUnicode { get; private set; } = string.Empty; 67 | public string Title { get; private set; } = string.Empty; 68 | public string TitleUnicode { get; private set; } = string.Empty; 69 | public string AudioFilename { get; private set; } = string.Empty; 70 | public string BackgroundFilename { get; private set; } = string.Empty; 71 | public string VideoFilename { get; private set; } = string.Empty; 72 | 73 | /// 74 | /// Return the first of all possible beatmap set paths. 75 | /// If not found.return string.Empty. 76 | /// 77 | public string Folder { get; private set; } = string.Empty; 78 | 79 | public int OsuClientID { get; private set; } 80 | public string Filename { get; private set; } = string.Empty; 81 | public string FilenameFull { get; private set; } = string.Empty; 82 | 83 | private static readonly Beatmap s_empty = new Beatmap(0, -1, -1, null); 84 | public static Beatmap Empty => s_empty; 85 | 86 | public Beatmap(int osu_id, int set_id, int id, FileStream fs) 87 | { 88 | BeatmapSetID = set_id; 89 | OsuClientID = osu_id; 90 | BeatmapID = id; 91 | 92 | if (fs != null) 93 | { 94 | Folder = Path.GetDirectoryName(fs.Name); 95 | Filename = Path.GetFileName(fs.Name); 96 | FilenameFull = fs.Name; 97 | 98 | using (var sr = new StreamReader(fs)) 99 | { 100 | string block = ""; 101 | 102 | while(!sr.EndOfStream) 103 | { 104 | string line = sr.ReadLine().Trim(); 105 | if (line.StartsWith("[")&&line.EndsWith("]")) 106 | { 107 | block = line; 108 | } 109 | else if(block== "[General]"||block== "[Metadata]") 110 | { 111 | foreach(var prop in typeof(Beatmap).GetProperties()) 112 | { 113 | if (line.StartsWith($"{prop.Name}:")) 114 | { 115 | object val=GetPropertyValue(line); 116 | if (prop.PropertyType == typeof(int)) 117 | val = int.Parse(val as string); 118 | if (prop.PropertyType == typeof(double)) 119 | val = double.Parse(val as string); 120 | prop.SetValue(this, val); 121 | } 122 | } 123 | } 124 | else if(block=="[Events]") 125 | { 126 | if(line.StartsWith("Video")) 127 | { 128 | var breaked=line.Split(','); 129 | VideoFilename = breaked[2].Replace("\"","").Trim(); 130 | } 131 | else if(line.StartsWith("0,")&&string.IsNullOrEmpty(BackgroundFilename)) 132 | { 133 | var breaked = line.Split(','); 134 | BackgroundFilename = breaked[2].Replace("\"", "").Trim(); 135 | } 136 | } 137 | } 138 | } 139 | } 140 | } 141 | 142 | public static bool operator ==(Beatmap a, Beatmap b) 143 | { 144 | if(a is null && b is null) 145 | { 146 | return true; 147 | } 148 | if(a is null && !(b is null)|| 149 | !(a is null) && b is null) 150 | { 151 | return false; 152 | } 153 | return a.Equals(b); 154 | } 155 | 156 | public static bool operator !=(Beatmap a, Beatmap b) 157 | { 158 | if (a is null && b is null) 159 | { 160 | return false; 161 | } 162 | if (a is null && !(b is null) || 163 | !(a is null) && b is null) 164 | { 165 | return true; 166 | } 167 | return !a.Equals(b); 168 | } 169 | 170 | private static string GetPropertyValue(string line) 171 | { 172 | int pos = line.IndexOf(':'); 173 | return line.Substring(pos + 1).Trim(); 174 | } 175 | 176 | public override bool Equals(object obj) 177 | { 178 | if (obj is Beatmap beatmap) 179 | { 180 | return BeatmapID == beatmap.BeatmapID && 181 | BeatmapSetID == beatmap.BeatmapSetID && 182 | Difficulty == beatmap.Difficulty && 183 | Creator == beatmap.Creator && 184 | Artist == beatmap.Artist && 185 | Title == beatmap.Title && 186 | Filename == beatmap.Filename; 187 | } 188 | return false; 189 | } 190 | 191 | public override int GetHashCode() 192 | { 193 | var hashCode = -173464191; 194 | hashCode = hashCode * -1521134295 + BeatmapID.GetHashCode(); 195 | hashCode = hashCode * -1521134295 + BeatmapSetID.GetHashCode(); 196 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Difficulty); 197 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Creator); 198 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Artist); 199 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Title); 200 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Filename); 201 | return hashCode; 202 | } 203 | } 204 | } -------------------------------------------------------------------------------- /DefaultLanguage.cs: -------------------------------------------------------------------------------- 1 | using Sync.Tools; 2 | using Sync.Tools.ConfigurationAttribute; 3 | 4 | namespace OsuRTDataProvider 5 | { 6 | public class DefaultLanguage : I18nProvider 7 | { 8 | public static LanguageElement LANG_OSU_NOT_FOUND = "[ID:{0}]Not found osu!.exe process"; 9 | public static LanguageElement LANG_OSU_FOUND = "[ID:{0}]Found osu!.exe process"; 10 | public static LanguageElement LANG_TOURNEY_HINT = "Tourney Mode: {0}"; 11 | 12 | public static LanguageElement CHECK_GOTO_RELEASE_PAGE_HINT = "Enter \"ortdp releases\" to open the releases page in your browser."; 13 | public static LanguageElement LANG_CHECK_ORTDP_UPDATE = "Found a new version of OsuRTDataProvider({0})"; 14 | public static LanguageElement LANG_INIT_STATUS_FINDER_FAILED = "[ID:{0}]Init StatusFinder Failed! Retry after {1} seconds"; 15 | public static LanguageElement LANG_INIT_STATUS_FINDER_SUCCESS = "[ID:{0}]Init StatusFinder Success!"; 16 | public static LanguageElement LANG_INIT_PLAY_FINDER_FAILED = "[ID:{0}]Init PlayFinder Failed! Retry after {1} seconds"; 17 | public static LanguageElement LANG_INIT_PLAY_FINDER_SUCCESS = "[ID:{0}]Init PlayFinder Success!"; 18 | public static LanguageElement LANG_INIT_BEATMAP_FINDER_FAILED = "[ID:{0}]Init BeatmapFinder Failed! Retry after {1} seconds"; 19 | public static LanguageElement LANG_INIT_BEATMAP_FINDER_SUCCESS = "[ID:{0}]Init BeatmapFinder Success!"; 20 | public static LanguageElement LANG_INIT_MODE_FINDER_FAILED = "[ID:{0}]Init ModeFinder Failed! Retry after {1} seconds"; 21 | public static LanguageElement LANG_INIT_MODE_FINDER_SUCCESS = "[ID:{0}]Init ModeFinder Success!"; 22 | public static LanguageElement LANG_INIT_HIT_EVENT_SUCCESS = "[ID:{0}]Init HitEventFinder Success!"; 23 | public static LanguageElement LANG_INIT_HIT_EVENT_FAIL = "[ID:{0}]Init HitEventFinder Failed! Retry after {1} seconds"; 24 | 25 | public static LanguageElement LANG_BEATMAP_NOT_FOUND = "Beatmap not found"; 26 | 27 | public static GuiLanguageElement ListenInterval = "Listen interval(ms)"; 28 | public static GuiLanguageElement EnableTourneyMode = "Tourney mode"; 29 | public static GuiLanguageElement TeamSize = "Team size"; 30 | public static GuiLanguageElement DebugMode = "Debug mode"; 31 | public static GuiLanguageElement ForceOsuSongsDirectory = "Force OSU! songs directory"; 32 | public static GuiLanguageElement GameMode = "Game Mode"; 33 | public static GuiLanguageElement DisableProcessNotFoundInformation = "Disable OSU! process not found information"; 34 | public static GuiLanguageElement EnableModsChangedAtListening = "Enable Mods Changed At Listening(Experimental)"; 35 | } 36 | } -------------------------------------------------------------------------------- /Helper/HardwareInformationHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Management; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace OsuRTDataProvider.Helper 9 | { 10 | static class HardwareInformationHelper 11 | { 12 | public static string GetPhysicalMemory() 13 | { 14 | ManagementScope oMs = new ManagementScope(); 15 | ObjectQuery oQuery = new ObjectQuery("SELECT Capacity FROM Win32_PhysicalMemory"); 16 | ManagementObjectSearcher oSearcher = new ManagementObjectSearcher(oMs, oQuery); 17 | ManagementObjectCollection oCollection = oSearcher.Get(); 18 | 19 | long MemSize = 0; 20 | long mCap = 0; 21 | 22 | // In case more than one Memory sticks are installed 23 | foreach (ManagementObject obj in oCollection) 24 | { 25 | mCap = Convert.ToInt64(obj["Capacity"]); 26 | MemSize += mCap; 27 | } 28 | MemSize = (MemSize / 1024) / 1024; 29 | return MemSize.ToString() + "MB"; 30 | } 31 | 32 | public static String GetProcessorInformation() 33 | { 34 | ManagementClass mc = new ManagementClass("win32_processor"); 35 | ManagementObjectCollection moc = mc.GetInstances(); 36 | String info = String.Empty; 37 | foreach (ManagementObject mo in moc) 38 | { 39 | string name = (string)mo["Name"]; 40 | 41 | info = name + ", " + (string)mo["Caption"] + ", " + (string)mo["SocketDesignation"]; 42 | 43 | break; 44 | } 45 | return info; 46 | } 47 | 48 | public static string GetOSInformation() 49 | { 50 | ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT * FROM Win32_OperatingSystem"); 51 | foreach (ManagementObject wmi in searcher.Get()) 52 | { 53 | try 54 | { 55 | return ((string)wmi["Caption"]).Trim() + ", " + (string)wmi["Version"] + ", " + (string)wmi["OSArchitecture"]; 56 | } 57 | catch { } 58 | } 59 | return "BIOS Maker: Unknown"; 60 | } 61 | 62 | private static void Print(string str) 63 | { 64 | #if !DEBUG 65 | Sync.Tools.IO.FileLogger.Write($"{str}"); 66 | #else 67 | Sync.Tools.IO.CurrentIO.Write($"{str}"); 68 | #endif 69 | } 70 | 71 | public static void PrintHardwareInformation() 72 | { 73 | Print($"CLI: {Environment.Version}"); 74 | Print($"OS: {GetOSInformation()}"); 75 | Print($"CPU: {GetProcessorInformation()}"); 76 | Print($"Memory: {GetPhysicalMemory()} Total"); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Helper/OsuStatusHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using OsuRTDataProvider.Listen; 7 | 8 | using static OsuRTDataProvider.Listen.OsuListenerManager; 9 | 10 | namespace OsuRTDataProvider.Helper 11 | { 12 | public static class OsuStatusHelper 13 | { 14 | public static bool IsListening(OsuStatus status) 15 | { 16 | const OsuStatus listen = OsuStatus.SelectSong | OsuStatus.MatchSetup | OsuStatus.Lobby | OsuStatus.Idle; 17 | return (listen & status) == status; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Helper/PathHelper.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | 4 | namespace OsuRTDataProvider.Helper 5 | { 6 | internal static class PathHelper 7 | { 8 | public static string WindowsPathStrip(string entry) 9 | { 10 | StringBuilder builder = new StringBuilder(entry); 11 | foreach (char c in Path.GetInvalidFileNameChars()) 12 | builder.Replace(c.ToString(), string.Empty); 13 | builder.Replace(".", string.Empty); 14 | return builder.ToString(); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /Language/en-US/OsuRTDataProvider.DefaultLanguage.lang: -------------------------------------------------------------------------------- 1 | [en-US] 2 | LANG_OSU_NOT_FOUND=[ID:{0}]Cannot find osu!.exe process 3 | LANG_OSU_FOUND=[ID:{0}]Found osu!.exe process 4 | LANG_INIT_PLAY_FINDER_FAILED=[ID:{0}]Init PlayFinder Failed! Retry after {1} seconds 5 | LANG_INIT_PLAY_FINDER_SUCCESS=[ID:{0}]Init PlayFinder Success! 6 | LANG_INIT_STATUS_FINDER_FAILED=[ID:{0}]Init StatusFinder Failed! Retry after {1} seconds 7 | LANG_INIT_STATUS_FINDER_SUCCESS=[ID:{0}]Init StatusFinder Success! 8 | LANG_INIT_BEATMAP_FINDER_FAILED=[ID:{0}]Init BeatmapFinder Failed! Retry after {1} seconds 9 | LANG_INIT_BEATMAP_FINDER_SUCCESS=[ID:{0}]Init BeatmapFinder Success! 10 | LANG_INIT_MODE_FINDER_FAILED=[ID:{0}]Init ModeFinder Failed! Retry after {1} seconds 11 | LANG_INIT_MODE_FINDER_SUCCESS=[ID:{0}]Init ModeFinder Success! 12 | LANG_INIT_HIT_EVENT_SUCCESS=[ID:{0}]Init HitEventFinder Success! 13 | LANG_INIT_HIT_EVENT_FAIL=[ID:{0}]Init HitEventFinder Failed! Retry after {1} seconds 14 | LANG_BEATMAP_NOT_FOUND=Beatmap not found 15 | ListenInterval=Listen interval(ms) 16 | EnableTourneyMode=Tourney mode 17 | TeamSize=Team size 18 | DebugMode=Debug mode 19 | ForceOsuSongsDirectory=Force OSU! songs directory 20 | -------------------------------------------------------------------------------- /Language/ja-JP/OsuRTDataProvider.DefaultLanguage.lang: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OsuSync/OsuRTDataProvider/09eac48de7086e527b4228eeaa4a788742d8a1e1/Language/ja-JP/OsuRTDataProvider.DefaultLanguage.lang -------------------------------------------------------------------------------- /Language/zh-CN/OsuRTDataProvider.DefaultLanguage.lang: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OsuSync/OsuRTDataProvider/09eac48de7086e527b4228eeaa4a788742d8a1e1/Language/zh-CN/OsuRTDataProvider.DefaultLanguage.lang -------------------------------------------------------------------------------- /Listen/ErrorStatisticsResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace OsuRTDataProvider.Listen 8 | { 9 | public struct ErrorStatisticsResult : IEquatable 10 | { 11 | public static ErrorStatisticsResult Empty => new ErrorStatisticsResult(); 12 | public double ErrorMin, ErrorMax; 13 | public double UnstableRate; 14 | 15 | public bool Equals(ErrorStatisticsResult other) 16 | { 17 | return Math.Abs(other.ErrorMin - ErrorMin) < 1e-6 && 18 | Math.Abs(other.ErrorMax - ErrorMax) < 1e-6 && 19 | Math.Abs(other.UnstableRate - UnstableRate) < 1e-6; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Listen/HitEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace OsuRTDataProvider.Listen 8 | { 9 | public enum PlayType 10 | { 11 | Playing = 0, 12 | Replay = 1, 13 | 14 | Unknown = -1 15 | } 16 | 17 | [Flags] 18 | public enum KeysDownFlags 19 | { 20 | M1 = 1, 21 | M2 = 2, 22 | K1 = 4, 23 | K2 = 8, 24 | Smoke = 16, 25 | } 26 | 27 | public class HitEvent 28 | { 29 | public int TimeStamp { get; } 30 | public KeysDownFlags KeysDown => (KeysDownFlags)Z; 31 | public float X { get; } 32 | public float Y { get; } 33 | private int Z; 34 | 35 | 36 | public HitEvent(float x, float y, int z, int timeStamp) 37 | { 38 | this.X = x; 39 | this.Y = y; 40 | this.Z = z; 41 | this.TimeStamp = timeStamp; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Listen/OSUListenerManager.cs: -------------------------------------------------------------------------------- 1 | using OsuRTDataProvider.BeatmapInfo; 2 | using OsuRTDataProvider.Helper; 3 | using OsuRTDataProvider.Memory; 4 | using OsuRTDataProvider.Mods; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.ComponentModel; 8 | using System.Diagnostics; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Management; 12 | using System.Text; 13 | using System.Threading; 14 | using System.Threading.Tasks; 15 | using static OsuRTDataProvider.DefaultLanguage; 16 | 17 | namespace OsuRTDataProvider.Listen 18 | { 19 | public class OsuListenerManager 20 | { 21 | [Flags] 22 | public enum OsuStatus:UInt64 23 | { 24 | NoFoundProcess = 1ul<<0, 25 | Unkonwn = 1ul<<1, 26 | SelectSong = 1ul<<2, 27 | Playing = 1ul<<3, 28 | Editing = 1ul<<4, 29 | Rank = 1ul<<5, 30 | MatchSetup = 1ul<<6, 31 | Lobby = 1ul<<7, 32 | Idle = 1ul<<8, 33 | } 34 | 35 | private static Dictionary s_game_mode_map = new Dictionary(4) 36 | { 37 | ["Osu"] = OsuPlayMode.Osu, 38 | ["Taiko"] = OsuPlayMode.Taiko, 39 | ["CatchTheBeat"] = OsuPlayMode.CatchTheBeat, 40 | ["Mania"] = OsuPlayMode.Mania, 41 | }; 42 | 43 | static private List> s_listen_update_list = new List>(); 44 | static private Task s_listen_task; 45 | static private bool s_stop_flag = false; 46 | 47 | #region Event 48 | 49 | public delegate void OnPlayModeChangedEvt(OsuPlayMode last, OsuPlayMode mode); 50 | 51 | public delegate void OnStatusChangedEvt(OsuStatus last_status, OsuStatus status); 52 | 53 | public delegate void OnBeatmapChangedEvt(Beatmap map); 54 | 55 | public delegate void OnHealthPointChangedEvt(double hp); 56 | 57 | public delegate void OnAccuracyChangedEvt(double acc); 58 | 59 | public delegate void OnComboChangedEvt(int combo); 60 | 61 | public delegate void OnModsChangedEvt(ModsInfo mods); 62 | 63 | public delegate void OnPlayingTimeChangedEvt(int ms); 64 | 65 | public delegate void OnHitCountChangedEvt(int hit); 66 | 67 | public delegate void OnErrorStatisticsChangedEvt(ErrorStatisticsResult result); 68 | 69 | public delegate void OnPlayerChangedEvt(string player); 70 | 71 | public delegate void OnHitEventsChangedEvt(PlayType playType, List hitEvents); 72 | 73 | /// 74 | /// Available in Playing and Linsten. 75 | /// If too old beatmap, map.ID = -1. 76 | /// 77 | public event Action OnScoreChanged; 78 | 79 | /// 80 | /// Available in Linsten. 81 | /// 82 | public event OnPlayModeChangedEvt OnPlayModeChanged; 83 | 84 | /// 85 | /// Available in Playing and Linsten. 86 | /// If too old beatmap, map.ID = -1. 87 | /// 88 | public event OnBeatmapChangedEvt OnBeatmapChanged; 89 | 90 | /// 91 | /// Available in Playing. 92 | /// 93 | public event OnHealthPointChangedEvt OnHealthPointChanged; 94 | 95 | /// 96 | /// Available in Playing. 97 | /// 98 | public event OnAccuracyChangedEvt OnAccuracyChanged; 99 | 100 | /// 101 | /// Available in Playing. 102 | /// 103 | public event OnComboChangedEvt OnComboChanged; 104 | 105 | /// 106 | /// Available in Playing. 107 | /// if OsuStatus turns Listen , mods = ModsInfo.Empty 108 | /// 109 | public event OnModsChangedEvt OnModsChanged; 110 | 111 | /// 112 | /// Available in Playing and Listen. 113 | /// 114 | public event OnPlayingTimeChangedEvt OnPlayingTimeChanged; 115 | 116 | /// 117 | /// Available in Playing. 118 | /// 119 | public event OnHitCountChangedEvt OnCount300Changed; 120 | 121 | /// 122 | /// Available in Playing. 123 | /// 124 | public event OnHitCountChangedEvt OnCount100Changed; 125 | 126 | /// 127 | /// Available in Playing. 128 | /// 129 | public event OnHitCountChangedEvt OnCount50Changed; 130 | 131 | /// 132 | /// Mania: 300g 133 | /// 134 | public event OnHitCountChangedEvt OnCountGekiChanged; 135 | 136 | /// 137 | /// Mania: 200 138 | /// 139 | public event OnHitCountChangedEvt OnCountKatuChanged; 140 | 141 | /// 142 | /// Available in Playing. 143 | /// 144 | public event OnHitCountChangedEvt OnCountMissChanged; 145 | 146 | /// 147 | /// Get Game Status. 148 | /// 149 | public event OnStatusChangedEvt OnStatusChanged; 150 | 151 | /// 152 | /// Get ErrorStatistics(UnstableRate and Error Hit). 153 | /// 154 | public event OnErrorStatisticsChangedEvt OnErrorStatisticsChanged; 155 | 156 | /// 157 | /// Get player name in playing. 158 | /// 159 | public event OnPlayerChangedEvt OnPlayerChanged; 160 | 161 | /// 162 | /// Get play type and hit events in playing. 163 | /// 164 | public event OnHitEventsChangedEvt OnHitEventsChanged; 165 | #endregion Event 166 | 167 | private Process m_osu_process; 168 | 169 | private OsuPlayFinder m_play_finder = null; 170 | private OsuStatusFinder m_status_finder = null; 171 | private OsuBeatmapFinder m_beatmap_finder = null; 172 | private OsuPlayModeFinder m_mode_finder = null; 173 | private OsuHitEventFinder m_hit_event_finder = null; 174 | 175 | #region last status 176 | 177 | private OsuStatus m_last_osu_status = OsuStatus.Unkonwn; 178 | private string m_last_playername = string.Empty; 179 | private OsuPlayMode m_last_mode = OsuPlayMode.Unknown; 180 | private Beatmap m_last_beatmap = Beatmap.Empty; 181 | private ModsInfo m_last_mods = ModsInfo.Empty; 182 | 183 | private double m_last_hp = 0; 184 | private double m_last_acc = 0; 185 | private ErrorStatisticsResult m_last_error_statistics = ErrorStatisticsResult.Empty; 186 | private int m_last_combo = 0; 187 | private int m_playing_time = 0; 188 | private int m_last_300 = 0; 189 | private int m_last_100 = 0; 190 | private int m_last_50 = 0; 191 | private int m_last_miss = 0; 192 | private int m_last_geki = 0; 193 | private int m_last_katu = 0; 194 | 195 | 196 | private int m_last_score = 0; 197 | 198 | private List m_hit_events = new List(); 199 | private PlayType m_play_type = PlayType.Unknown; 200 | 201 | #endregion last status 202 | 203 | private readonly bool m_is_tourney = false; 204 | private readonly int m_osu_id = 0; 205 | 206 | #region OsuRTDataProviderThread 207 | 208 | static OsuListenerManager() 209 | { 210 | s_stop_flag = false; 211 | 212 | //Listen Thread 213 | s_listen_task = Task.Run(() => 214 | { 215 | Thread.CurrentThread.Name = "OsuRTDataProviderThread"; 216 | Thread.Sleep(2000); 217 | while (!s_stop_flag) 218 | { 219 | for (int i = 0; i < s_listen_update_list.Count; i++) 220 | { 221 | var action = s_listen_update_list[i]; 222 | action.Item2(); 223 | } 224 | 225 | Thread.Sleep(Setting.ListenInterval); 226 | } 227 | }); 228 | } 229 | #endregion 230 | 231 | public OsuListenerManager(bool tourney = false, int osuid = 0) 232 | { 233 | m_is_tourney = tourney; 234 | m_osu_id = osuid; 235 | } 236 | 237 | public void Start() 238 | { 239 | s_listen_update_list.Add(new Tuple(m_osu_id, ListenLoopUpdate)); 240 | } 241 | 242 | public void Stop() 243 | { 244 | var tuple = s_listen_update_list.Where(t => t.Item1 == m_osu_id).FirstOrDefault(); 245 | s_listen_update_list.Remove(tuple); 246 | 247 | if (s_listen_update_list.Count == 0) 248 | { 249 | s_stop_flag = true; 250 | s_listen_task.Wait(); 251 | } 252 | } 253 | 254 | #region Get Current Data 255 | 256 | private bool HasMask(ProvideDataMask mask, ProvideDataMask h) 257 | { 258 | return (mask & h) == h; 259 | } 260 | 261 | public ProvideData GetCurrentData(ProvideDataMask mask) 262 | { 263 | ProvideData data = new ProvideData(); 264 | 265 | data.ClientID = m_osu_id; 266 | data.Status = m_last_osu_status; 267 | 268 | data.PlayMode = OsuPlayMode.Unknown; 269 | data.Beatmap = Beatmap.Empty; 270 | data.Mods = ModsInfo.Empty; 271 | 272 | data.Combo = 0; 273 | data.Count300 = 0; 274 | data.Count100 = 0; 275 | data.Count50 = 0; 276 | data.CountMiss = 0; 277 | data.CountGeki = 0; 278 | data.CountKatu = 0; 279 | data.HealthPoint = 0; 280 | data.Accuracy = 0; 281 | data.Time = 0; 282 | data.Score = 0; 283 | 284 | data.PlayType = PlayType.Unknown; 285 | data.HitEvents = new List(); 286 | 287 | if (HasMask(mask, ProvideDataMask.Beatmap)) 288 | { 289 | if (OnBeatmapChanged == null) OnBeatmapChanged += (t) => { }; 290 | data.Beatmap = m_last_beatmap; 291 | } 292 | 293 | if (HasMask(mask, ProvideDataMask.HealthPoint)) 294 | { 295 | if (OnHealthPointChanged == null) OnHealthPointChanged += (t) => { }; 296 | data.HealthPoint = m_last_hp; 297 | } 298 | 299 | if (HasMask(mask, ProvideDataMask.Accuracy)) 300 | { 301 | if (OnAccuracyChanged == null) OnAccuracyChanged += (t) => { }; 302 | data.Accuracy = m_last_acc; 303 | } 304 | 305 | if (HasMask(mask, ProvideDataMask.Combo)) 306 | { 307 | if (OnComboChanged == null) OnComboChanged += (t) => { }; 308 | data.Combo = m_last_combo; 309 | } 310 | 311 | if (HasMask(mask, ProvideDataMask.Count300)) 312 | { 313 | if (OnCount300Changed == null) OnCount300Changed += (t) => { }; 314 | data.Count300 = m_last_300; 315 | } 316 | 317 | if (HasMask(mask, ProvideDataMask.Count100)) 318 | { 319 | if (OnCount100Changed == null) OnCount100Changed += (t) => { }; 320 | data.Count100 = m_last_100; 321 | } 322 | 323 | if (HasMask(mask, ProvideDataMask.Count50)) 324 | { 325 | if (OnCount50Changed == null) OnCount50Changed += (t) => { }; 326 | data.Count50 = m_last_50; 327 | } 328 | 329 | if (HasMask(mask, ProvideDataMask.CountMiss)) 330 | { 331 | if (OnCountMissChanged == null) OnCountMissChanged += (t) => { }; 332 | data.CountMiss = m_last_miss; 333 | } 334 | 335 | if (HasMask(mask, ProvideDataMask.CountGeki)) 336 | { 337 | if (OnCountGekiChanged == null) OnCountGekiChanged += (t) => { }; 338 | data.CountGeki = m_last_geki; 339 | } 340 | 341 | if (HasMask(mask, ProvideDataMask.CountKatu)) 342 | { 343 | if (OnCountKatuChanged == null) OnCountKatuChanged += (t) => { }; 344 | data.CountKatu = m_last_katu; 345 | } 346 | 347 | if (HasMask(mask, ProvideDataMask.Time)) 348 | { 349 | if (OnPlayingTimeChanged == null) OnPlayingTimeChanged += (t) => { }; 350 | data.Time = m_playing_time; 351 | } 352 | 353 | if (HasMask(mask, ProvideDataMask.Mods)) 354 | { 355 | if (OnModsChanged == null) OnModsChanged += (t) => { }; 356 | data.Mods = m_last_mods; 357 | } 358 | 359 | if (HasMask(mask, ProvideDataMask.GameMode)) 360 | { 361 | if (OnPlayModeChanged == null) OnPlayModeChanged += (t, t2) => { }; 362 | data.PlayMode = m_last_mode; 363 | } 364 | 365 | if (HasMask(mask, ProvideDataMask.Score)) 366 | { 367 | if (OnScoreChanged == null) OnScoreChanged += (s) => { }; 368 | data.Score = m_last_score; 369 | } 370 | 371 | if (HasMask(mask, ProvideDataMask.ErrorStatistics)) 372 | { 373 | if (OnErrorStatisticsChanged == null) OnErrorStatisticsChanged += (e) => { }; 374 | data.ErrorStatistics = m_last_error_statistics; 375 | } 376 | 377 | if (HasMask(mask, ProvideDataMask.Playername)) 378 | { 379 | if (OnPlayerChanged == null) OnPlayerChanged += (e) => { }; 380 | data.Playername = m_last_playername; 381 | } 382 | 383 | if (HasMask(mask, ProvideDataMask.HitEvent)) 384 | { 385 | if (OnHitEventsChanged == null) OnHitEventsChanged += (t, l) => { }; 386 | data.PlayType = m_play_type; 387 | data.HitEvents = m_hit_events; 388 | } 389 | 390 | return data; 391 | } 392 | 393 | #endregion Get Current Data 394 | 395 | #region Init Finder 396 | private const long RETRY_INTERVAL = 3000; 397 | 398 | private Dictionary finder_timer_dict = new Dictionary(); 399 | private T InitFinder(string success_fmt, string failed_fmt) where T : OsuFinderBase 400 | { 401 | if (!finder_timer_dict.ContainsKey(typeof(T))) 402 | finder_timer_dict.Add(typeof(T), 0); 403 | 404 | T finder = null; 405 | long timer = finder_timer_dict[typeof(T)]; 406 | 407 | if (timer % RETRY_INTERVAL == 0) 408 | { 409 | finder = typeof(T).GetConstructors()[0].Invoke(new object[] { m_osu_process }) as T; 410 | if (finder.TryInit()) 411 | { 412 | timer = 0; 413 | Logger.Info(string.Format(success_fmt, m_osu_id)); 414 | return finder; 415 | } 416 | 417 | finder = null; 418 | Logger.Error(string.Format(failed_fmt, m_osu_id, RETRY_INTERVAL / 1000)); 419 | } 420 | timer += Setting.ListenInterval; 421 | finder_timer_dict[typeof(T)] = timer; 422 | return finder; 423 | } 424 | #endregion 425 | 426 | #region Find Osu Setting 427 | private long find_osu_process_timer = 0; 428 | private const long FIND_OSU_RETRY_INTERVAL = 5000; 429 | 430 | private void FindOsuProcess() 431 | { 432 | if (find_osu_process_timer > FIND_OSU_RETRY_INTERVAL) 433 | { 434 | find_osu_process_timer = 0; 435 | Process[] process_list; 436 | 437 | process_list = Process.GetProcessesByName("osu!"); 438 | 439 | if (s_stop_flag) return; 440 | if (process_list.Length != 0) 441 | { 442 | if (m_is_tourney) 443 | { 444 | foreach (var p in process_list) 445 | { 446 | if (p.MainWindowTitle.Contains($"Client {m_osu_id}")) 447 | { 448 | m_osu_process = p; 449 | break; 450 | } 451 | } 452 | } 453 | else 454 | { 455 | m_osu_process = process_list[0]; 456 | } 457 | 458 | if (m_osu_process != null) 459 | { 460 | FindSongPathAndUsername(); 461 | Logger.Info(string.Format(LANG_OSU_FOUND, m_osu_id)); 462 | return; 463 | } 464 | } 465 | find_osu_process_timer = 0; 466 | if (!Setting.DisableProcessNotFoundInformation) 467 | Logger.Error(string.Format(LANG_OSU_NOT_FOUND, m_osu_id)); 468 | } 469 | find_osu_process_timer += Setting.ListenInterval; 470 | } 471 | 472 | private void FindSongPathAndUsername() 473 | { 474 | string osu_path = ""; 475 | find_osu_filename: 476 | try 477 | { 478 | osu_path = Path.GetDirectoryName(m_osu_process.MainModule.FileName); 479 | } 480 | catch (Win32Exception e) 481 | { 482 | if(Setting.DebugMode) 483 | Logger.Warn($"Win32Exception: {e.ToString()}"); 484 | Logger.Warn("Can't get osu path, Retry after 2 seconds."); 485 | Thread.Sleep(2000); 486 | goto find_osu_filename; 487 | } 488 | string osu_config_file = Path.Combine(osu_path, $"osu!.{PathHelper.WindowsPathStrip(Environment.UserName)}.cfg"); 489 | string song_path; 490 | 491 | 492 | try 493 | { 494 | using (var fs = File.OpenRead(osu_config_file)) 495 | using (var sr = new StreamReader(fs)) 496 | { 497 | while (!sr.EndOfStream) 498 | { 499 | string line = sr.ReadLine(); 500 | 501 | if (line.StartsWith("BeatmapDirectory")) 502 | { 503 | if (Directory.Exists(Setting.ForceOsuSongsDirectory)) 504 | { 505 | Setting.SongsPath = Setting.ForceOsuSongsDirectory; 506 | } 507 | else 508 | { 509 | Logger.Info($"ForceOsuSongsDirectory: {Setting.ForceOsuSongsDirectory}"); 510 | Logger.Info($"The ForceOsuSongsDirectory does not exist, try searching for the songs path."); 511 | song_path = line.Split('=')[1].Trim(); 512 | if (Path.IsPathRooted(song_path)) 513 | Setting.SongsPath = song_path; 514 | else 515 | Setting.SongsPath = Path.Combine(osu_path, song_path); 516 | } 517 | } 518 | else if (line.StartsWith("Username")) 519 | { 520 | Setting.Username = line.Split('=')[1].Trim(); 521 | } 522 | else if (line.StartsWith("LastVersion")&&!line.StartsWith("LastVersionPermissionsFailed")) 523 | { 524 | Setting.OsuVersion = line.Split('=')[1].Trim(); 525 | Logger.Info($"OSU Client Verison:{Setting.OsuVersion} ORTDP Version:{OsuRTDataProviderPlugin.VERSION}"); 526 | } 527 | } 528 | } 529 | } 530 | catch (Exception e) 531 | { 532 | Logger.Error($"Exception: {e.ToString()}"); 533 | } 534 | 535 | if (string.IsNullOrWhiteSpace(Setting.SongsPath)) 536 | { 537 | Logger.Warn($"Search failed, use default songs path."); 538 | Setting.SongsPath = Path.Combine(osu_path,"Songs"); 539 | } 540 | Logger.Info($"Osu Path: {osu_path}"); 541 | Logger.Info($"Beatmap Path: {Setting.SongsPath}"); 542 | } 543 | #endregion 544 | 545 | private void ListenLoopUpdate() 546 | { 547 | OsuStatus status = GetCurrentOsuStatus(); 548 | 549 | if (status == OsuStatus.NoFoundProcess) 550 | { 551 | m_osu_process = null; 552 | m_play_finder = null; 553 | m_status_finder = null; 554 | m_beatmap_finder = null; 555 | m_mode_finder = null; 556 | m_hit_event_finder = null; 557 | 558 | FindOsuProcess(); 559 | } 560 | 561 | //Waiting for osu to start 562 | if (status != OsuStatus.NoFoundProcess && status != OsuStatus.Unkonwn) 563 | { 564 | //Wait for player to playing 565 | if (status == OsuStatus.Playing) 566 | { 567 | if (m_play_finder == null) 568 | { 569 | m_play_finder = InitFinder(LANG_INIT_PLAY_FINDER_SUCCESS, LANG_INIT_PLAY_FINDER_FAILED); 570 | } 571 | 572 | if (Setting.GameMode == "Auto" && m_mode_finder == null) 573 | { 574 | m_mode_finder = InitFinder(LANG_INIT_MODE_FINDER_SUCCESS, LANG_INIT_MODE_FINDER_FAILED); 575 | } 576 | 577 | if (m_hit_event_finder == null) 578 | { 579 | m_hit_event_finder = InitFinder(LANG_INIT_HIT_EVENT_SUCCESS, LANG_INIT_HIT_EVENT_FAIL); 580 | } 581 | } 582 | 583 | if (m_beatmap_finder == null) 584 | { 585 | m_beatmap_finder = InitFinder(LANG_INIT_BEATMAP_FINDER_SUCCESS, LANG_INIT_BEATMAP_FINDER_FAILED); 586 | } 587 | 588 | if (m_mode_finder != null) 589 | { 590 | OsuPlayMode mode = OsuPlayMode.Osu; 591 | 592 | if (OnPlayModeChanged != null) mode = m_mode_finder.GetMode(); 593 | 594 | if (m_last_mode != mode) 595 | OnPlayModeChanged?.Invoke(m_last_mode, mode); 596 | 597 | m_last_mode = mode; 598 | } 599 | else 600 | { 601 | if (Setting.GameMode != "Auto") 602 | { 603 | if (s_game_mode_map.TryGetValue(Setting.GameMode, out var mode)) 604 | if (m_last_mode != mode) 605 | OnPlayModeChanged?.Invoke(m_last_mode, mode); 606 | 607 | m_last_mode = mode; 608 | } 609 | } 610 | 611 | if (OnHitEventsChanged != null && m_hit_event_finder != null) 612 | { 613 | // Hit events should work with playing time 614 | if (OnPlayingTimeChanged == null) OnPlayingTimeChanged += (t) => { }; 615 | 616 | bool hasChanged; 617 | m_hit_event_finder.GetHitEvents(status, m_playing_time, out m_play_type, out m_hit_events, out hasChanged); 618 | if (hasChanged) 619 | { 620 | OnHitEventsChanged?.Invoke(m_play_type, m_hit_events); 621 | } 622 | } 623 | 624 | if (m_play_finder != null) 625 | { 626 | Beatmap beatmap = Beatmap.Empty; 627 | ModsInfo mods = ModsInfo.Empty; 628 | ErrorStatisticsResult error_statistics = ErrorStatisticsResult.Empty; 629 | int cb = 0; 630 | int pt = 0; 631 | int n300 = 0; 632 | int n100 = 0; 633 | int n50 = 0; 634 | int ngeki = 0; 635 | int nkatu = 0; 636 | int nmiss = 0; 637 | int score = 0; 638 | double hp = 0.0; 639 | double acc = 0.0; 640 | string playername = Setting.Username; 641 | 642 | try 643 | { 644 | if (OnPlayingTimeChanged != null) pt = m_play_finder.GetPlayingTime(); 645 | if (OnBeatmapChanged != null) beatmap = m_beatmap_finder.GetCurrentBeatmap(m_osu_id); 646 | if (Setting.EnableModsChangedAtListening && status != OsuStatus.Playing) 647 | if (OnModsChanged != null) mods = m_play_finder.GetCurrentModsAtListening(); 648 | 649 | if (beatmap != Beatmap.Empty && beatmap != m_last_beatmap) 650 | { 651 | OnBeatmapChanged?.Invoke(beatmap); 652 | } 653 | 654 | if (status == OsuStatus.Playing) 655 | { 656 | if (OnErrorStatisticsChanged != null) error_statistics = m_play_finder.GetUnstableRate(); 657 | if (OnModsChanged != null) mods = m_play_finder.GetCurrentMods(); 658 | if (OnComboChanged != null) cb = m_play_finder.GetCurrentCombo(); 659 | if (OnCount300Changed != null) n300 = m_play_finder.Get300Count(); 660 | if (OnCount100Changed != null) n100 = m_play_finder.Get100Count(); 661 | if (OnCount50Changed != null) n50 = m_play_finder.Get50Count(); 662 | if (OnCountGekiChanged != null) ngeki = m_play_finder.GetGekiCount(); 663 | if (OnCountKatuChanged != null) nkatu = m_play_finder.GetKatuCount(); 664 | if (OnCountMissChanged != null) nmiss = m_play_finder.GetMissCount(); 665 | if (OnAccuracyChanged != null) acc = m_play_finder.GetCurrentAccuracy(); 666 | if (OnHealthPointChanged != null) hp = m_play_finder.GetCurrentHP(); 667 | if (OnScoreChanged != null) score = m_play_finder.GetCurrentScore(); 668 | if (OnPlayerChanged != null) playername = m_play_finder.GetCurrentPlayerName(); 669 | } 670 | 671 | if (status != m_last_osu_status) 672 | OnStatusChanged?.Invoke(m_last_osu_status, status); 673 | 674 | if (mods != ModsInfo.Empty && !ModsInfo.VaildMods(mods)) 675 | mods = m_last_mods; 676 | 677 | if (mods != m_last_mods) 678 | OnModsChanged?.Invoke(mods); 679 | 680 | if (hp != m_last_hp) 681 | OnHealthPointChanged?.Invoke(hp); 682 | 683 | if (acc != m_last_acc) 684 | OnAccuracyChanged?.Invoke(acc); 685 | 686 | if (!error_statistics.Equals(m_last_error_statistics)) 687 | OnErrorStatisticsChanged?.Invoke(error_statistics); 688 | 689 | if (playername != m_last_playername) 690 | OnPlayerChanged?.Invoke(playername); // Added null check 691 | 692 | if (score != m_last_score) 693 | OnScoreChanged?.Invoke(score); 694 | 695 | if (n300 != m_last_300) 696 | OnCount300Changed?.Invoke(n300); 697 | 698 | if (n100 != m_last_100) 699 | OnCount100Changed?.Invoke(n100); 700 | 701 | if (n50 != m_last_50) 702 | OnCount50Changed?.Invoke(n50); 703 | 704 | if (ngeki != m_last_geki) 705 | OnCountGekiChanged?.Invoke(ngeki); 706 | 707 | if (nkatu != m_last_katu) 708 | OnCountKatuChanged?.Invoke(nkatu); 709 | 710 | if (nmiss != m_last_miss) 711 | OnCountMissChanged?.Invoke(nmiss); 712 | 713 | if (cb != m_last_combo) 714 | OnComboChanged?.Invoke(cb); 715 | 716 | if (pt != m_playing_time) 717 | OnPlayingTimeChanged?.Invoke(pt); 718 | } 719 | catch (Exception e) 720 | { 721 | Logger.Error(e.ToString()); 722 | } 723 | 724 | m_last_beatmap = beatmap; 725 | m_last_mods = mods; 726 | m_last_hp = hp; 727 | m_last_acc = acc; 728 | m_last_error_statistics = error_statistics; 729 | m_last_combo = cb; 730 | m_playing_time = pt; 731 | m_last_300 = n300; 732 | m_last_100 = n100; 733 | m_last_50 = n50; 734 | m_last_geki = ngeki; 735 | m_last_katu = nkatu; 736 | m_last_miss = nmiss; 737 | m_last_score = score; 738 | m_last_osu_status = status; 739 | m_last_playername = playername; 740 | } 741 | } 742 | } 743 | private OsuStatus GetCurrentOsuStatus() 744 | { 745 | try 746 | { 747 | if (m_osu_process == null) return OsuStatus.NoFoundProcess; 748 | if (m_osu_process.HasExited == true) return OsuStatus.NoFoundProcess; 749 | 750 | if (m_status_finder == null) 751 | { 752 | m_status_finder = InitFinder( 753 | LANG_INIT_STATUS_FINDER_SUCCESS, 754 | LANG_INIT_STATUS_FINDER_FAILED 755 | ); 756 | return OsuStatus.Unkonwn; 757 | } 758 | } 759 | catch (Win32Exception e) 760 | { 761 | Logger.Error($":{e.Message}"); 762 | } 763 | 764 | OsuInternalStatus mode = m_status_finder.GetCurrentOsuModes(); 765 | switch (mode) 766 | { 767 | case OsuInternalStatus.Edit: 768 | return OsuStatus.Editing; 769 | 770 | case OsuInternalStatus.Play: 771 | return OsuStatus.Playing; 772 | 773 | case OsuInternalStatus.RankingTagCoop: 774 | case OsuInternalStatus.RankingTeam: 775 | case OsuInternalStatus.RankingVs: 776 | case OsuInternalStatus.Rank: 777 | return OsuStatus.Rank; 778 | 779 | case OsuInternalStatus.BeatmapImport: 780 | case OsuInternalStatus.Menu: 781 | case OsuInternalStatus.OnlineSelection: 782 | return OsuStatus.Idle; 783 | 784 | case OsuInternalStatus.MatchSetup: 785 | return OsuStatus.MatchSetup; 786 | 787 | case OsuInternalStatus.Lobby: 788 | return OsuStatus.Lobby; 789 | 790 | case OsuInternalStatus.SelectEdit: 791 | case OsuInternalStatus.SelectMulti: 792 | case OsuInternalStatus.SelectPlay: 793 | return OsuStatus.SelectSong; 794 | 795 | default: 796 | return OsuStatus.Unkonwn; 797 | } 798 | } 799 | } 800 | } 801 | -------------------------------------------------------------------------------- /Listen/PlayMode.cs: -------------------------------------------------------------------------------- 1 | namespace OsuRTDataProvider.Listen 2 | { 3 | public enum OsuPlayMode 4 | { 5 | Osu = 0, 6 | Taiko = 1, 7 | CatchTheBeat = 2, 8 | Mania = 3, 9 | 10 | Unknown = -1 11 | } 12 | } -------------------------------------------------------------------------------- /Listen/ProvideData.cs: -------------------------------------------------------------------------------- 1 | using OsuRTDataProvider.BeatmapInfo; 2 | using OsuRTDataProvider.Mods; 3 | using System; 4 | using System.Collections.Generic; 5 | using static OsuRTDataProvider.Listen.OsuListenerManager; 6 | 7 | namespace OsuRTDataProvider.Listen 8 | { 9 | [Flags] 10 | public enum ProvideDataMask : uint 11 | { 12 | HealthPoint = 1u << 0, 13 | Accuracy = 1u << 1, 14 | Combo = 1u << 2, 15 | Count300 = 1u << 3, 16 | Count100 = 1u << 4, 17 | Count50 = 1u << 5, 18 | CountMiss = 1u << 6, 19 | CountGeki = 1u << 7, 20 | CountKatu = 1u << 8, 21 | Time = 1u << 9, 22 | Mods = 1u << 10, 23 | GameMode = 1u << 11, 24 | Beatmap = 1u << 12, 25 | Score = 1u << 13, 26 | ErrorStatistics = 1u<<14, 27 | Playername = 1u<<15, 28 | HitEvent = 1u << 16, 29 | 30 | HitCount = Count300 | Count100 | Count50 | CountMiss | CountGeki | CountKatu, 31 | } 32 | 33 | public class ProvideData 34 | { 35 | public int ClientID; 36 | public OsuStatus Status; 37 | public string Playername; 38 | 39 | public OsuPlayMode PlayMode; 40 | public Beatmap Beatmap; 41 | public ModsInfo Mods; 42 | public ErrorStatisticsResult ErrorStatistics; 43 | 44 | public double HealthPoint; 45 | public double Accuracy; 46 | public int Combo; 47 | public int Count300; 48 | public int Count100; 49 | public int Count50; 50 | public int CountMiss; 51 | public int CountGeki; 52 | public int CountKatu; 53 | public int Time; 54 | public int Score; 55 | 56 | public PlayType PlayType; 57 | public List HitEvents; 58 | } 59 | } -------------------------------------------------------------------------------- /Memory/BeatmapOffsetInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Dynamic; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace OsuRTDataProvider.Memory 8 | { 9 | 10 | public class BeatmapOffsetInfo 11 | { 12 | public enum CompareCondition 13 | { 14 | None, 15 | Older, 16 | OlderOrEquals, 17 | Equals, 18 | NewerOrEquals, 19 | Newer 20 | } 21 | 22 | public double Version { get; set; } 23 | public int BeatmapAddressOffset { get; set; } 24 | public int BeatmapSetAddressOffset { get; set; } 25 | public int BeatmapFolderAddressOffset { get; set; } 26 | public int BeatmapFileNameAddressOffset { get; set; } 27 | public CompareCondition VersionCompareCondition { get; set; } 28 | public void AddOffset(BeatmapOffsetInfo beatmapOffsetInfo) 29 | { 30 | BeatmapAddressOffset += beatmapOffsetInfo.BeatmapAddressOffset; 31 | BeatmapFolderAddressOffset += beatmapOffsetInfo.BeatmapFolderAddressOffset; 32 | BeatmapSetAddressOffset += beatmapOffsetInfo.BeatmapSetAddressOffset; 33 | BeatmapFileNameAddressOffset += beatmapOffsetInfo.BeatmapFileNameAddressOffset; 34 | } 35 | 36 | public void SetOffset(BeatmapOffsetInfo beatmapOffsetInfo) 37 | { 38 | BeatmapAddressOffset = beatmapOffsetInfo.BeatmapAddressOffset; 39 | BeatmapFolderAddressOffset = beatmapOffsetInfo.BeatmapFolderAddressOffset; 40 | BeatmapSetAddressOffset = beatmapOffsetInfo.BeatmapSetAddressOffset; 41 | BeatmapFileNameAddressOffset = beatmapOffsetInfo.BeatmapFileNameAddressOffset; 42 | } 43 | 44 | 45 | public override string ToString() => $"{VersionCompareCondition} " + 46 | $"{Version} " + 47 | $"BeatmapAddress: {BeatmapAddressOffset} " + 48 | $"BeatmapFolder: {BeatmapAddressOffset} " + 49 | $"BeatmapSet: {BeatmapSetAddressOffset} " + 50 | $"BeatmapFileName: {BeatmapFileNameAddressOffset}"; 51 | 52 | public static BeatmapOffsetInfo MatchVersion(double version) 53 | { 54 | Dictionary propertyForVersions = new Dictionary(); 55 | Type t = typeof(BeatmapOffsetInfo); 56 | var methods = t.GetProperties(BindingFlags.Static | BindingFlags.Public); 57 | foreach (var method in methods) 58 | { 59 | if (!method.IsDefined(typeof(OsuVersionCompareInfoAttribute), false)) 60 | { 61 | continue; 62 | } 63 | 64 | var attr = method.GetCustomAttribute(); 65 | 66 | propertyForVersions.Add(attr, method); 67 | } 68 | propertyForVersions = propertyForVersions.OrderByDescending(item => item.Key.OsuVersion). 69 | ToDictionary(item => item.Key,item => item.Value); 70 | 71 | bool versionMatched = false; 72 | foreach (var compareInfo in propertyForVersions) 73 | { 74 | double comparedVersion = compareInfo.Key.OsuVersion; 75 | switch (compareInfo.Key.CompareCondition) 76 | { 77 | case CompareCondition.Older: 78 | versionMatched = comparedVersion < version; 79 | break; 80 | case CompareCondition.OlderOrEquals: 81 | versionMatched = comparedVersion <= version; 82 | break; 83 | case CompareCondition.Newer: 84 | versionMatched = comparedVersion > version; 85 | break; 86 | case CompareCondition.NewerOrEquals: 87 | versionMatched = comparedVersion <= version; 88 | break; 89 | case CompareCondition.Equals: 90 | //https://www.jetbrains.com/help/resharper/2022.1/CompareOfFloatsByEqualityOperator.html 91 | versionMatched = Math.Abs(comparedVersion - version) < 0.00001; 92 | break; 93 | } 94 | 95 | if (versionMatched) 96 | { 97 | return (BeatmapOffsetInfo)compareInfo.Value.GetValue(null); 98 | } 99 | } 100 | 101 | 102 | return new BeatmapOffsetInfo {Version = version}; 103 | } 104 | 105 | [OsuVersionCompareInfo(20190816, CompareCondition.Older)] 106 | public static BeatmapOffsetInfo Version20190816 { get; } = new BeatmapOffsetInfo 107 | { 108 | Version = 20190816, 109 | BeatmapFileNameAddressOffset = -4, 110 | BeatmapSetAddressOffset = -4, 111 | BeatmapAddressOffset = -4, 112 | BeatmapFolderAddressOffset = -4, 113 | VersionCompareCondition = CompareCondition.Older 114 | }; 115 | 116 | 117 | [OsuVersionCompareInfo(20211014, CompareCondition.NewerOrEquals)] 118 | public static BeatmapOffsetInfo Version20211014 { get; } = new BeatmapOffsetInfo 119 | { 120 | Version = 20211014, 121 | BeatmapFileNameAddressOffset = 0, 122 | BeatmapSetAddressOffset = 0, 123 | BeatmapAddressOffset = 4, 124 | BeatmapFolderAddressOffset = 0, 125 | VersionCompareCondition = CompareCondition.NewerOrEquals 126 | }; 127 | 128 | [OsuVersionCompareInfo(20220406.3, CompareCondition.NewerOrEquals)] 129 | public static BeatmapOffsetInfo Version202204063 { get; } = new BeatmapOffsetInfo 130 | { 131 | Version = 20220406.3, 132 | BeatmapFileNameAddressOffset = 8, 133 | BeatmapSetAddressOffset = 0, 134 | BeatmapAddressOffset = 0, 135 | BeatmapFolderAddressOffset = 4, 136 | VersionCompareCondition = CompareCondition.NewerOrEquals 137 | }; 138 | 139 | 140 | } 141 | } -------------------------------------------------------------------------------- /Memory/LogHelper.cs: -------------------------------------------------------------------------------- 1 | using Sync.Tools; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Security.Cryptography; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace OsuRTDataProvider.Memory 10 | { 11 | static class LogHelper 12 | { 13 | public static void LogToFile(string plainText) 14 | { 15 | /* 16 | string msg = plainText; 17 | #if DEBUG 18 | ISyncOutput output = IO.CurrentIO; 19 | #else 20 | ISyncOutput output = IO.FileLogger; 21 | #endif 22 | 23 | output.Write($"{msg}"); 24 | */ 25 | 26 | //now is directly output to default in DebugMode 27 | Logger.Debug(plainText); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Memory/OsuBeatmapFinder.cs: -------------------------------------------------------------------------------- 1 | using OsuRTDataProvider.BeatmapInfo; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Globalization; 6 | using System.IO; 7 | using System.Text; 8 | 9 | namespace OsuRTDataProvider.Memory 10 | { 11 | internal class OsuBeatmapFinder : OsuFinderBase 12 | { 13 | 14 | private static readonly string s_beatmap_pattern = "\x74\x24\x8B\x0D\x0\x0\x0\x0\x85\xC9\x74\x1A"; 15 | 16 | private static readonly string s_beatmap_mask = "xxxx????xxxx"; 17 | 18 | private static readonly int s_beatmap_offset = 0xc4; 19 | private static readonly int s_beatmap_set_offset = 0xc8; 20 | 21 | private static readonly int s_beatmap_folder_offset = 0x74; 22 | private static readonly int s_beatmap_filename_offset = 0x8c; 23 | 24 | private BeatmapOffsetInfo CurrentOffset { get; } = new BeatmapOffsetInfo() 25 | { 26 | BeatmapAddressOffset = s_beatmap_offset, 27 | BeatmapSetAddressOffset = s_beatmap_set_offset, 28 | BeatmapFolderAddressOffset = s_beatmap_folder_offset, 29 | BeatmapFileNameAddressOffset = s_beatmap_filename_offset 30 | }; 31 | 32 | private const int MAX_RETRY_COUNT = 10; 33 | 34 | private IntPtr m_beatmap_address; 35 | 36 | public OsuBeatmapFinder(Process osu) : base(osu) 37 | { 38 | var versionBeatmapOffset = BeatmapOffsetInfo.MatchVersion(Setting.CurrentOsuVersionValue); 39 | CurrentOffset.AddOffset(versionBeatmapOffset); 40 | Logger.Info($"applied offset for osu!version({Setting.CurrentOsuVersionValue.ToString(CultureInfo.InvariantCulture)}) : {versionBeatmapOffset}"); 41 | } 42 | 43 | 44 | public override bool TryInit() 45 | { 46 | bool success = false; 47 | SigScan.Reload(); 48 | { 49 | //Find Beatmap ID Address 50 | m_beatmap_address = SigScan.FindPattern(StringToByte(s_beatmap_pattern), s_beatmap_mask, 4); 51 | LogHelper.LogToFile($"Beatmap Base Address (0):0x{(int)m_beatmap_address:X8}"); 52 | 53 | success = TryReadIntPtrFromMemory(m_beatmap_address, out m_beatmap_address); 54 | LogHelper.LogToFile($"Beatmap Base Address (1):0x{(int)m_beatmap_address:X8}"); 55 | } 56 | SigScan.ResetRegion(); 57 | 58 | if (m_beatmap_address == IntPtr.Zero) 59 | success = false; 60 | 61 | return success; 62 | } 63 | 64 | public Beatmap GetCurrentBeatmap(int osu_id) 65 | { 66 | TryReadIntPtrFromMemory(m_beatmap_address, out IntPtr cur_beatmap_address); 67 | TryReadIntFromMemory(cur_beatmap_address + CurrentOffset.BeatmapAddressOffset, out int id); 68 | TryReadIntFromMemory(cur_beatmap_address + CurrentOffset.BeatmapSetAddressOffset, out int set_id); 69 | 70 | string filename = GetCurrentBeatmapFilename(); 71 | string folder = GetCurrentBeatmapFolder(); 72 | 73 | Beatmap beatmap = Beatmap.Empty; 74 | 75 | try 76 | { 77 | if (!(string.IsNullOrWhiteSpace(filename) || string.IsNullOrWhiteSpace(folder))) 78 | { 79 | string folder_full = Path.Combine(Setting.SongsPath, folder); 80 | string filename_full = Path.Combine(folder_full, filename); 81 | using (var fs = File.OpenRead(filename_full)) 82 | { 83 | beatmap = new Beatmap(osu_id, set_id, id, fs); 84 | } 85 | } 86 | } 87 | catch (Exception e) 88 | { 89 | StringBuilder sb = new StringBuilder(); 90 | 91 | sb.AppendLine("------------- ORTDP(Exception)--------------- "); 92 | sb.AppendLine(e.ToString()); 93 | 94 | if (Setting.DebugMode) 95 | { 96 | sb.AppendLine("--------------ORTDP(Detail)-----------------"); 97 | sb.AppendLine($"Songs Path:{Setting.SongsPath}"); 98 | sb.AppendLine($"Filename:{filename}"); 99 | sb.AppendLine($"Folder:{folder}"); 100 | sb.AppendLine($"BeatmapID:{id}"); 101 | sb.AppendLine($"BeatmapSetID:{set_id}"); 102 | sb.AppendLine("--------------------------------------------"); 103 | } 104 | 105 | Logger.Warn(sb.ToString()); 106 | } 107 | 108 | return beatmap; 109 | } 110 | 111 | #region Beatmap Info 112 | 113 | private string GetCurrentBeatmapFolder() 114 | { 115 | TryReadIntPtrFromMemory(m_beatmap_address, out var cur_beatmap_address); 116 | bool success = TryReadStringFromMemory(cur_beatmap_address + CurrentOffset.BeatmapFolderAddressOffset, out string str); 117 | if (!success) return ""; 118 | return str; 119 | } 120 | 121 | private string GetCurrentBeatmapFilename() 122 | { 123 | TryReadIntPtrFromMemory(m_beatmap_address, out var cur_beatmap_address); 124 | bool success = TryReadStringFromMemory(cur_beatmap_address + CurrentOffset.BeatmapFileNameAddressOffset, out string str); 125 | if (!success) return ""; 126 | return str; 127 | } 128 | 129 | #endregion Beatmap Info 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Memory/OsuFinderBase.cs: -------------------------------------------------------------------------------- 1 | using Sync.Tools; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Runtime.InteropServices; 6 | using System.Security.Cryptography; 7 | using System.Text; 8 | 9 | namespace OsuRTDataProvider.Memory 10 | { 11 | internal abstract class OsuFinderBase 12 | { 13 | protected SigScan SigScan { get; private set; } 14 | protected Process OsuProcess { get; private set; } 15 | 16 | private int max_bytes_length = 4096; 17 | private byte[] _bytes_buf = new byte[4096]; 18 | 19 | private const int STRING_BUFFER_LENGTH_MAX = 4096; 20 | private byte[] _string_bytes_buf = new byte[4096]; 21 | 22 | public int ReadBufferLengthMax 23 | { 24 | get => max_bytes_length; 25 | set 26 | { 27 | max_bytes_length = value; 28 | _bytes_buf = new byte[value]; 29 | } 30 | } 31 | 32 | public OsuFinderBase(Process process) 33 | { 34 | OsuProcess = process; 35 | SigScan = new SigScan(OsuProcess); 36 | } 37 | 38 | private List _a = new List(64); 39 | 40 | protected byte[] StringToByte(string s) 41 | { 42 | _a.Clear(); 43 | foreach (var c in s) _a.Add((byte)c); 44 | return _a.ToArray(); 45 | } 46 | 47 | private byte[] _number_buf = new byte[8]; 48 | 49 | protected bool TryReadIntPtrFromMemory(IntPtr address, out IntPtr value) 50 | { 51 | int ret_size_ptr = 0; 52 | value = IntPtr.Zero; 53 | 54 | if (SigScan.ReadProcessMemory(OsuProcess.Handle, address, _number_buf, sizeof(int), out ret_size_ptr)) 55 | { 56 | value = (IntPtr)BitConverter.ToInt32(_number_buf, 0); 57 | return true; 58 | } 59 | return false; 60 | } 61 | 62 | protected bool TryReadIntFromMemory(IntPtr address, out int value) 63 | { 64 | int ret_size_ptr = 0; 65 | value = 0; 66 | 67 | if (SigScan.ReadProcessMemory(OsuProcess.Handle, address, _number_buf, sizeof(int), out ret_size_ptr)) 68 | { 69 | value = BitConverter.ToInt32(_number_buf, 0); 70 | return true; 71 | } 72 | return false; 73 | } 74 | 75 | protected bool TryReadShortFromMemory(IntPtr address, out ushort value) 76 | { 77 | int ret_size_ptr = 0; 78 | value = 0; 79 | 80 | if (SigScan.ReadProcessMemory(OsuProcess.Handle, address, _number_buf, sizeof(ushort), out ret_size_ptr)) 81 | { 82 | value = BitConverter.ToUInt16(_number_buf, 0); 83 | return true; 84 | } 85 | return false; 86 | } 87 | 88 | protected bool TryReadDoubleFromMemory(IntPtr address, out double value) 89 | { 90 | int ret_size_ptr = 0; 91 | value = double.NaN; 92 | 93 | if (SigScan.ReadProcessMemory(OsuProcess.Handle, address, _number_buf, sizeof(double), out ret_size_ptr)) 94 | { 95 | value = BitConverter.ToDouble(_number_buf, 0); 96 | return true; 97 | } 98 | return false; 99 | } 100 | 101 | protected bool TryReadSingleFromMemory(IntPtr address, out float value) 102 | { 103 | int ret_size_ptr = 0; 104 | value = float.NaN; 105 | 106 | if (SigScan.ReadProcessMemory(OsuProcess.Handle, address, _number_buf, sizeof(float), out ret_size_ptr)) 107 | { 108 | value = BitConverter.ToSingle(_number_buf, 0); 109 | return true; 110 | } 111 | return false; 112 | } 113 | 114 | protected bool TryReadStringFromMemory(IntPtr address, out string str) 115 | { 116 | str = null; 117 | TryReadIntPtrFromMemory(address, out IntPtr str_base); 118 | 119 | try 120 | { 121 | if (!TryReadIntFromMemory(str_base + 0x4, out int len)) 122 | return false; 123 | 124 | len *= 2; 125 | if (len > STRING_BUFFER_LENGTH_MAX || len <= 0) return false; 126 | 127 | if (SigScan.ReadProcessMemory(OsuProcess.Handle, str_base + 0x8, _string_bytes_buf, (uint)len, out int ret_size)) 128 | { 129 | if (len == ret_size) 130 | { 131 | str = Encoding.Unicode.GetString(_string_bytes_buf, 0, len); 132 | return true; 133 | } 134 | } 135 | } 136 | catch (Exception) 137 | { 138 | return false; 139 | } 140 | 141 | return false; 142 | } 143 | 144 | protected bool TryReadListFromMemory(IntPtr address, out List list)where T:struct 145 | { 146 | list = null; 147 | int type_size = Marshal.SizeOf(); 148 | TryReadIntPtrFromMemory(address, out IntPtr list_ptr); 149 | 150 | try 151 | { 152 | if(!TryReadIntFromMemory(list_ptr + 0xc, out int len)) 153 | return false; 154 | if (len <= 0) return false; 155 | 156 | int bytes = len * type_size; 157 | if (bytes > ReadBufferLengthMax) 158 | { 159 | ReadBufferLengthMax = (int)(bytes * 1.5); 160 | } 161 | 162 | TryReadIntPtrFromMemory(list_ptr + 0x4, out var array_ptr); 163 | 164 | if (SigScan.ReadProcessMemory(OsuProcess.Handle, array_ptr + 0x8, _bytes_buf, (uint)bytes, out int ret_size)) 165 | { 166 | if (bytes == ret_size) 167 | { 168 | T[] data = new T[len]; 169 | Buffer.BlockCopy(_bytes_buf, 0, data, 0, bytes); 170 | list = new List(data); 171 | return true; 172 | } 173 | } 174 | } 175 | catch (Exception) 176 | { 177 | return false; 178 | } 179 | 180 | return false; 181 | } 182 | 183 | public abstract bool TryInit(); 184 | } 185 | } -------------------------------------------------------------------------------- /Memory/OsuHitEventFinder.cs: -------------------------------------------------------------------------------- 1 | using OsuRTDataProvider.Listen; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using static OsuRTDataProvider.Listen.OsuListenerManager; 6 | 7 | namespace OsuRTDataProvider.Memory 8 | { 9 | 10 | internal class OsuHitEventFinder : OsuFinderBase 11 | { 12 | private OsuReplayHitEventFinder replay; 13 | private OsuPlayingHitEventFinder playing; 14 | private OsuStatus preStatus = OsuStatus.Unkonwn; 15 | private long preTime = long.MaxValue; 16 | private PlayType currentPlayType = PlayType.Unknown; 17 | public static readonly List EMPTY_EVENTS = new List(); 18 | 19 | public OsuHitEventFinder(Process process) : base(process) 20 | { 21 | replay = new OsuReplayHitEventFinder(process); 22 | playing = new OsuPlayingHitEventFinder(process); 23 | } 24 | 25 | public override bool TryInit() 26 | { 27 | bool success = true; 28 | success &= replay.TryInit(); 29 | success &= playing.TryInit(); 30 | return success; 31 | } 32 | 33 | public void GetHitEvents(OsuStatus osuStatus, long playTime, out PlayType playType, out List hitEvents, out bool hasChanged) 34 | { 35 | hasChanged = false; 36 | playType = PlayType.Unknown; 37 | hitEvents = EMPTY_EVENTS; 38 | if (osuStatus != OsuStatus.Playing) 39 | { 40 | hasChanged = this.preStatus == OsuStatus.Playing; 41 | if (hasChanged) Logger.Debug($"Hit events changed due to osustatus: {OsuStatus.Playing} -> {osuStatus}"); 42 | 43 | this.preStatus = osuStatus; 44 | this.currentPlayType = PlayType.Unknown; 45 | return; 46 | } 47 | 48 | if (this.preStatus != OsuStatus.Playing || preTime > playTime) 49 | { 50 | replay.Clear(); 51 | playing.Clear(); 52 | if (preStatus != OsuStatus.Playing) 53 | Logger.Debug($"Hit events changed due to osustatus: {preStatus} -> {OsuStatus.Playing}"); 54 | else 55 | Logger.Debug($"Hit events changed due to playing time: {preTime} -> {playTime}"); 56 | hasChanged = true; 57 | } 58 | this.preStatus = OsuStatus.Playing; 59 | this.preTime = playTime; 60 | 61 | if (currentPlayType == PlayType.Replay || currentPlayType == PlayType.Unknown) 62 | { 63 | hasChanged |= replay.GetEvents(out hitEvents); 64 | currentPlayType = hitEvents.Count == 0 ? PlayType.Unknown : PlayType.Replay; 65 | } 66 | 67 | if (currentPlayType == PlayType.Playing || currentPlayType == PlayType.Unknown) 68 | { 69 | hasChanged |= playing.GetEvents(out hitEvents); 70 | currentPlayType = hitEvents.Count == 0 ? PlayType.Unknown : PlayType.Playing; 71 | } 72 | 73 | playType = currentPlayType; 74 | } 75 | } 76 | 77 | internal class OsuReplayHitEventFinder : BaseOsuHitEventFinder 78 | { 79 | // D9 5D C0 EB 4E A1 ?? ?? ?? ?? 8B 48 34 4E 80 | // 74 4D A1 ?? ?? ?? ?? 8B 58 34 8D 46 FF 81 | // A1 ?? ?? ?? ?? 8B 40 34 8B 70 0C 82 | // 75 0E 33 D2 89 15 ?? ?? ?? ?? 89 15 83 | internal override string[] pattern => new string[] { 84 | "\xD9\x5D\xC0\xEB\x4E\xA1\x00\x00\x00\x00\x8B\x48\x34\x4E", 85 | "\x74\x4D\xA1\x00\x00\x00\x00\x8B\x58\x34\x8D\x46\xFF", 86 | "\xA1\x00\x00\x00\x00\x8B\x40\x34\x8B\x70\x0C", 87 | "\x75\x0E\x33\xD2\x89\x15\x0\x0\x0\x0\x89\x15" 88 | }; 89 | 90 | internal override string[] mask => new string[] { 91 | "xxxxxx????xxxx", "xxx????xxxxxx", "x????xxxxxx", "xxxxxx????xx" 92 | }; 93 | 94 | internal override int[] offset => new int[] { 6, 3, 1, 6 }; 95 | 96 | internal override string name => "Replay"; 97 | 98 | public OsuReplayHitEventFinder(Process osu) : base(osu) 99 | { 100 | } 101 | } 102 | 103 | internal class OsuPlayingHitEventFinder : BaseOsuHitEventFinder 104 | { 105 | // 83 7E 60 00 74 2C A1 ?? ?? ?? ?? 8B 50 1C 8B 4A 04 106 | // 5D C3 A1 ?? ?? ?? ?? 8B 50 1C 8B 4A 04 107 | internal override string[] pattern => new string[] { 108 | "\x83\x7E\x60\x00\x74\x2C\xA1\x00\x00\x00\x00\x8B\x50\x1C\x8B\x4A\x04", 109 | "\x5D\xC3\xA1\x00\x00\x00\x00\x8B\x50\x1C\x8B\x4A\x04" 110 | }; 111 | 112 | internal override string[] mask => new string[] { 113 | "xxxxxxx????xxxxxx", "xxx????xxxxxx" 114 | }; 115 | 116 | internal override int[] offset => new int[] { 7, 3 }; 117 | 118 | internal override string name => "Playing"; 119 | 120 | public OsuPlayingHitEventFinder(Process osu) : base(osu) 121 | { 122 | } 123 | } 124 | 125 | internal abstract class BaseOsuHitEventFinder : OsuFinderBase 126 | { 127 | 128 | IntPtr[] Addresses = new IntPtr[5] { IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero }; 129 | int[] PreOffsets = new int[4] { -1, -1, -1, -1 }; 130 | 131 | internal abstract string[] pattern { get; } 132 | internal abstract string[] mask { get; } 133 | internal abstract int[] offset { get; } 134 | internal abstract string name { get; } 135 | 136 | private List CurrentEvents = new List(); 137 | 138 | public int GetOffset(int offsetDepth, int index) 139 | { 140 | switch (offsetDepth) 141 | { 142 | case 0: return 0x34; 143 | case 1: return 0x4; 144 | case 2: return 0x8 + index * 0x4; 145 | case 3: return 0; 146 | default: return -1; // this should not happen 147 | } 148 | } 149 | 150 | 151 | public BaseOsuHitEventFinder(Process osu) : base(osu) 152 | { 153 | } 154 | 155 | public override bool TryInit() 156 | { 157 | bool success = false; 158 | 159 | SigScan.Reload(); 160 | { 161 | for (int i = 0; i < pattern.Length; i++) 162 | { 163 | Addresses[0] = SigScan.FindPattern(StringToByte(pattern[i]), mask[i], offset[i]); 164 | success = Addresses[0] != IntPtr.Zero; 165 | 166 | if (!success) 167 | { 168 | continue; 169 | } 170 | 171 | success = TryReadIntPtrFromMemory(Addresses[0], out Addresses[0]); 172 | success &= Addresses[0] != IntPtr.Zero; 173 | if (!success) 174 | { 175 | continue; 176 | } 177 | 178 | LogHelper.LogToFile($"Hit Event ({name}) Base Address: 0x{(int)Addresses[0]:X8} by pattern #{i}"); 179 | break; 180 | } 181 | } 182 | SigScan.ResetRegion(); 183 | 184 | return success; 185 | } 186 | 187 | private HitEvent GetHitEvent(int index) 188 | { 189 | bool success = true, changed = false; 190 | for (int depth = 0; depth < 4; depth++) 191 | { 192 | int offset = GetOffset(depth, index); 193 | if (offset != PreOffsets[depth] || changed) 194 | { 195 | PreOffsets[depth] = offset; 196 | changed = true; 197 | success &= TryReadIntPtrFromMemory(Addresses[depth], out Addresses[depth + 1]); 198 | if (Addresses[depth + 1] == IntPtr.Zero || !success) 199 | { 200 | return null; 201 | } 202 | Addresses[depth + 1] = Addresses[depth + 1] + offset; 203 | 204 | //LogHelper.LogToFile($"Hit Event Base Address({depth + 1}): 0x{(int)Addresses[depth + 1]:X8}"); 205 | } 206 | } 207 | 208 | float x, y; 209 | int z; 210 | int timeStamp; 211 | success &= TryReadSingleFromMemory(Addresses[4] + 4, out x); 212 | success &= TryReadSingleFromMemory(Addresses[4] + 8, out y); 213 | success &= TryReadIntFromMemory(Addresses[4] + 12, out z); 214 | success &= TryReadIntFromMemory(Addresses[4] + 16, out timeStamp); 215 | 216 | if (success) 217 | { 218 | return new HitEvent(x, y, z, timeStamp); 219 | } 220 | return null; 221 | } 222 | 223 | public void Clear() 224 | { 225 | CurrentEvents.Clear(); 226 | } 227 | 228 | // Return if the events are changed. 229 | public bool GetEvents(out List hitEvents) 230 | { 231 | 232 | hitEvents = CurrentEvents; 233 | PreOffsets = new int[4] { -1, -1, -1, -1 }; 234 | 235 | int increment = 0; 236 | Stopwatch sw = new Stopwatch(); 237 | sw.Start(); 238 | 239 | while (true) 240 | { 241 | HitEvent hitEvent = GetHitEvent(CurrentEvents.Count); 242 | if (hitEvent == null) 243 | { 244 | break; 245 | } 246 | CurrentEvents.Add(hitEvent); 247 | increment = increment + 1; 248 | //LogHelper.LogToFile($"{increment}"); 249 | } 250 | 251 | sw.Stop(); 252 | long time = sw.ElapsedMilliseconds; 253 | 254 | return increment != 0; 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /Memory/OsuInternalStatus.cs: -------------------------------------------------------------------------------- 1 | namespace OsuRTDataProvider.Memory 2 | { 3 | internal enum OsuInternalStatus 4 | { 5 | Menu, 6 | Edit, 7 | Play, 8 | Exit, 9 | SelectEdit, 10 | SelectPlay, 11 | SelectDrawings, 12 | Rank, 13 | Update, 14 | Busy, 15 | Unknown, 16 | Lobby, 17 | MatchSetup, 18 | SelectMulti, 19 | RankingVs, 20 | OnlineSelection, 21 | OptionsOffsetWizard, 22 | RankingTagCoop, 23 | RankingTeam, 24 | BeatmapImport, 25 | PackageUpdater, 26 | Benchmark, 27 | Tourney, 28 | Charts 29 | }; 30 | } -------------------------------------------------------------------------------- /Memory/OsuModeFinder.cs: -------------------------------------------------------------------------------- 1 | using OsuRTDataProvider.Listen; 2 | using System; 3 | using System.Diagnostics; 4 | 5 | namespace OsuRTDataProvider.Memory 6 | { 7 | internal class OsuPlayModeFinder : OsuFinderBase 8 | { 9 | //85 ff 74 57? a1 ?? ?? ?? ?? 89 45 e4 10 | 11 | //private static readonly string s_mode_pattern = "\x73\x01\x00\x00\x83\x3D\x00\x00\x00\x00\x01\x0F\x84\x66\"; 12 | //private static readonly string s_mode_mask = "xxxxxx????xxxx"; 13 | //Another playmode pattern. 14 | //It can be used when the main pattern fails. 15 | private static readonly string s_mode_pattern = "\xEC\x57\x56\x53\x3B\x0D\x00\x00\x00\x00\x74\x60\x89\x0D"; 16 | 17 | private static readonly string s_mode_mask = "xxxxxx????xxxx"; 18 | 19 | private IntPtr m_mode_address; 20 | 21 | public OsuPlayModeFinder(Process process) : base(process) 22 | { 23 | } 24 | 25 | public override bool TryInit() 26 | { 27 | bool success = false; 28 | 29 | SigScan.Reload(); 30 | { 31 | m_mode_address = SigScan.FindPattern(StringToByte(s_mode_pattern), s_mode_mask,6); 32 | LogHelper.LogToFile($"Mode Address (0):0x{(int)m_mode_address:X8}"); 33 | 34 | success = TryReadIntPtrFromMemory(m_mode_address, out m_mode_address); 35 | LogHelper.LogToFile($"Mode Address (1):0x{(int)m_mode_address:X8}"); 36 | } 37 | SigScan.ResetRegion(); 38 | 39 | if (m_mode_address == IntPtr.Zero) 40 | success = false; 41 | 42 | return success; 43 | } 44 | 45 | public OsuPlayMode GetMode() 46 | { 47 | TryReadIntFromMemory(m_mode_address, out int val); 48 | return (OsuPlayMode)val; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /Memory/OsuPlayFinder.cs: -------------------------------------------------------------------------------- 1 | using OsuRTDataProvider.Listen; 2 | using OsuRTDataProvider.Mods; 3 | using System; 4 | using System.Diagnostics; 5 | 6 | namespace OsuRTDataProvider.Memory 7 | { 8 | internal class OsuPlayFinder : OsuFinderBase 9 | { 10 | #region Address Arguments 11 | 12 | //0xA1,0,0,0,0,0x8D,0x56,0x0C,0xE8,0x00,0x00,0x00,0x00,0x8B,0x47,0x04 13 | private static readonly string s_acc_pattern = "\xA1\x0\x0\x0\x0\x8D\x56\x0C\xE8\x00\x00\x00\x00\x8B\x47\x04"; 14 | private static readonly string s_acc_mask = "x????xxxx????xxx"; 15 | 16 | //0x73,0x7a,0x8b,0x0d,0x0,0x0,0x0,0x0,0x85,0xc9,0x74,0x1f 17 | private static readonly string s_acc_pattern_fallback = "\x73\x7a\x8b\x0d\x0\x0\x0\x0\x85\xc9\x74\x1f\x8d\x55\xf0"; 18 | private static readonly string s_acc_mask_fallback = "xxxx????xxxxxxx"; 19 | 20 | //0x5e,0x5f,0x5d,0xc3,0xa1,0x0,0x0,0x0,0x0,0x89,0x0,0x04 21 | private static readonly string s_time_pattern = "\x5e\x5f\x5d\xc3\xa1\x0\x0\x0\x0\x89\x0\x04"; 22 | private static readonly string s_time_mask = "xxxxx????x?x"; 23 | 24 | private static readonly string s_global_mods_pattern = "\x8B\xF1\xA1\x00\x00\x00\x00\x25\x00\x00\x40\x00\x85\xC0"; 25 | private static readonly string s_global_mods_mask = "xxx????xxxxxxx"; 26 | 27 | #endregion Address Arguments 28 | 29 | private IntPtr m_acc_address;//acc,combo,hp,mods,300hit,100hit,50hit,miss Base Address 30 | private IntPtr m_time_address; 31 | private IntPtr m_mods_address; 32 | 33 | public OsuPlayFinder(Process osu) : base(osu) 34 | { 35 | } 36 | 37 | public override bool TryInit() 38 | { 39 | bool success = false; 40 | bool m_accuracy_address_success = false; 41 | bool m_time_address_success = false; 42 | bool m_mods_address_success = false; 43 | 44 | SigScan.Reload(); 45 | { 46 | if (Setting.EnableModsChangedAtListening) 47 | { 48 | //Find mods address 49 | m_mods_address = SigScan.FindPattern(StringToByte(s_global_mods_pattern), s_global_mods_mask, 3); 50 | LogHelper.LogToFile($"Mods Base Address (0):0x{(int)m_mods_address:X8}"); 51 | 52 | m_mods_address_success = TryReadIntPtrFromMemory(m_mods_address, out m_mods_address); 53 | LogHelper.LogToFile($"Mods Base Address (1):0x{(int)m_mods_address:X8}"); 54 | } 55 | 56 | //Find acc Address 57 | m_acc_address = SigScan.FindPattern(StringToByte(s_acc_pattern), s_acc_mask, 1); 58 | LogHelper.LogToFile($"Playing Accuracy Base Address (0):0x{(int)m_acc_address:X8}"); 59 | 60 | m_accuracy_address_success = TryReadIntPtrFromMemory(m_acc_address, out m_acc_address); 61 | LogHelper.LogToFile($"Playing Accuracy Base Address (1):0x{(int)m_acc_address:X8}"); 62 | 63 | if (!m_accuracy_address_success)//use s_acc_pattern_fallback 64 | { 65 | LogHelper.LogToFile("Use Fallback Accuracy Pattern"); 66 | m_acc_address = SigScan.FindPattern(StringToByte(s_acc_pattern_fallback), s_acc_mask_fallback, 4); 67 | LogHelper.LogToFile($"Playing Accuracy Base Address (0):0x{(int)m_acc_address:X8}"); 68 | 69 | m_accuracy_address_success = TryReadIntPtrFromMemory(m_acc_address, out m_acc_address); 70 | LogHelper.LogToFile($"Playing Accuracy Base Address (1):0x{(int)m_acc_address:X8}"); 71 | } 72 | 73 | //Find Time Address 74 | m_time_address = SigScan.FindPattern(StringToByte(s_time_pattern), s_time_mask, 5); 75 | LogHelper.LogToFile($"Time Base Address (0):0x{(int)m_time_address:X8}"); 76 | 77 | m_time_address_success = TryReadIntPtrFromMemory(m_time_address, out m_time_address); 78 | LogHelper.LogToFile($"Time Base Address (1):0x{(int)m_time_address:X8}"); 79 | } 80 | SigScan.ResetRegion(); 81 | 82 | success = m_time_address_success && m_accuracy_address_success; 83 | if(Setting.EnableModsChangedAtListening) 84 | success = success && m_mods_address_success; 85 | 86 | if (m_acc_address == IntPtr.Zero || m_time_address == IntPtr.Zero) 87 | success = false; 88 | 89 | return success; 90 | } 91 | 92 | public double GetCurrentAccuracy() 93 | { 94 | TryReadIntPtrFromMemory(RulesetBaseAddress + 0x48, out var tmp_ptr); 95 | 96 | TryReadDoubleFromMemory(tmp_ptr + 0x14, out double value); 97 | return value; 98 | } 99 | 100 | public double GetCurrentHP() 101 | { 102 | TryReadIntPtrFromMemory(RulesetBaseAddress + 0x40, out var tmp_ptr); 103 | 104 | TryReadDoubleFromMemory(tmp_ptr + 0x1c, out double value); 105 | return value; 106 | } 107 | 108 | #region Score Address 109 | 110 | public int GetCurrentScore() 111 | { 112 | TryReadIntPtrFromMemory(m_acc_address, out var tmpPtr); 113 | 114 | TryReadIntPtrFromMemory(tmpPtr + 0x44, out tmpPtr); 115 | TryReadIntFromMemory(tmpPtr + 0xf8, out var value); 116 | return value; 117 | } 118 | 119 | public int GetCurrentCombo() 120 | { 121 | TryReadShortFromMemory(ScoreBaseAddress + 0x94, out var value); 122 | return value; 123 | } 124 | 125 | public int GetMissCount() 126 | { 127 | TryReadShortFromMemory(ScoreBaseAddress + 0x92, out var value); 128 | return value; 129 | } 130 | 131 | public int Get300Count() 132 | { 133 | TryReadShortFromMemory(ScoreBaseAddress + 0x8a, out ushort value); 134 | return value; 135 | } 136 | 137 | public int Get100Count() 138 | { 139 | TryReadShortFromMemory(ScoreBaseAddress + 0x88, out var value); 140 | return value; 141 | } 142 | 143 | public int Get50Count() 144 | { 145 | TryReadShortFromMemory(ScoreBaseAddress + 0x8c, out var value); 146 | return value; 147 | } 148 | 149 | /// 150 | /// Osu Geki 151 | /// Mania 300g 152 | /// 153 | /// 154 | public int GetGekiCount() 155 | { 156 | TryReadShortFromMemory(ScoreBaseAddress + 0x8e, out var value); 157 | return value; 158 | } 159 | 160 | /// 161 | /// Osu Katu 162 | /// Mania 200 163 | /// 164 | public int GetKatuCount() 165 | { 166 | TryReadShortFromMemory(ScoreBaseAddress + 0x90, out var value); 167 | return value; 168 | } 169 | 170 | public ErrorStatisticsResult GetUnstableRate() 171 | { 172 | TryReadListFromMemory(ScoreBaseAddress + 0x38, out var list); 173 | if (list == null) 174 | return ErrorStatisticsResult.Empty; 175 | var result = Utils.GetErrorStatisticsArray(list); 176 | return new ErrorStatisticsResult 177 | { 178 | ErrorMin = result[0], 179 | ErrorMax = result[1], 180 | UnstableRate = result[4] * 10, 181 | }; 182 | } 183 | 184 | public string GetCurrentPlayerName() 185 | { 186 | TryReadStringFromMemory(ScoreBaseAddress + 0x28, out var str); 187 | return str; 188 | } 189 | 190 | public ModsInfo GetCurrentModsAtListening() 191 | { 192 | if (TryReadIntFromMemory(m_mods_address, out var mods)) 193 | { 194 | //if (TryReadIntFromMemory(mods_ptr + 0x8, out int salt) && 195 | // TryReadIntFromMemory(mods_ptr + 0xc, out int mods)) 196 | //{ 197 | return new ModsInfo() 198 | { 199 | Mod = (ModsInfo.Mods)(mods) 200 | }; 201 | //} 202 | } 203 | return ModsInfo.Empty; 204 | } 205 | 206 | public ModsInfo GetCurrentMods() 207 | { 208 | IntPtr mods_ptr; 209 | 210 | var tmp_ptr = ScoreBaseAddress; 211 | TryReadIntPtrFromMemory(tmp_ptr + 0x1c, out mods_ptr); 212 | 213 | if (TryReadIntFromMemory(mods_ptr + 0x8, out int salt) && 214 | TryReadIntFromMemory(mods_ptr + 0xc, out int mods)) 215 | { 216 | return new ModsInfo() 217 | { 218 | Mod = (ModsInfo.Mods)(mods ^ salt) 219 | }; 220 | } 221 | return ModsInfo.Empty; 222 | } 223 | #endregion 224 | 225 | #region Time Address 226 | public int GetPlayingTime() 227 | { 228 | TryReadIntFromMemory(m_time_address, out int value); 229 | return value; 230 | } 231 | #endregion 232 | 233 | 234 | 235 | private IntPtr RulesetBaseAddress 236 | { 237 | get 238 | { 239 | TryReadIntPtrFromMemory(m_acc_address, out var tmp_ptr); 240 | return tmp_ptr; 241 | } 242 | } 243 | 244 | private IntPtr ScoreBaseAddress 245 | { 246 | get 247 | { 248 | TryReadIntPtrFromMemory(RulesetBaseAddress + 0x38, out var tmp_ptr); 249 | return tmp_ptr; 250 | } 251 | } 252 | } 253 | } -------------------------------------------------------------------------------- /Memory/OsuStatusFinder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace OsuRTDataProvider.Memory 5 | { 6 | internal class OsuStatusFinder : OsuFinderBase 7 | { 8 | private static readonly string s_game_modes_pattern = "\x75\x07\x8B\x45\x90\xC6\x40\x2A\x00\x83\x3D\x0\x0\x0\x0\x0F"; 9 | 10 | private static readonly string s_game_modes_mask = "xxxxxxxxxxx????x"; 11 | 12 | private IntPtr m_game_modes_address; 13 | private bool success = false; 14 | 15 | public OsuStatusFinder(Process osu) : base(osu) 16 | { 17 | } 18 | 19 | public override bool TryInit() 20 | { 21 | SigScan.Reload(); 22 | { 23 | //Find Game Modes 24 | m_game_modes_address = SigScan.FindPattern(StringToByte(s_game_modes_pattern), s_game_modes_mask, 11); 25 | LogHelper.LogToFile($"Game Status Address (0):0x{(int)m_game_modes_address:X8}"); 26 | 27 | success = TryReadIntPtrFromMemory(m_game_modes_address, out m_game_modes_address); 28 | LogHelper.LogToFile($"Game Status Address (1):0x{(int)m_game_modes_address:X8}"); 29 | } 30 | SigScan.ResetRegion(); 31 | 32 | if (m_game_modes_address == IntPtr.Zero) success = false; 33 | 34 | 35 | 36 | return success; 37 | } 38 | 39 | public OsuInternalStatus GetCurrentOsuModes() 40 | { 41 | TryReadIntFromMemory(m_game_modes_address, out int value); 42 | 43 | return (OsuInternalStatus)value; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /Memory/OsuVersionCompareInfoAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace OsuRTDataProvider.Memory 8 | { 9 | public class OsuVersionCompareInfoAttribute : Attribute 10 | { 11 | public OsuVersionCompareInfoAttribute(double osuVersion, BeatmapOffsetInfo.CompareCondition compareCondition) 12 | { 13 | OsuVersion = osuVersion; 14 | CompareCondition = compareCondition; 15 | } 16 | 17 | public double OsuVersion { get; } 18 | public BeatmapOffsetInfo.CompareCondition CompareCondition { get; } 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Memory/SigScan.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Dynamic; 5 | using System.Runtime.InteropServices; 6 | 7 | //Clone Form https://github.com/Deathmax/dxgw2/blob/master/Trainer_Rewrite/SigScan.cs 8 | //Modified by KedamaOvO 9 | 10 | // 11 | // sigScan C# Implementation - Written by atom0s [aka Wiccaan] 12 | // Class Version: 2.0.0 13 | // 14 | // [ CHANGE LOG ] ------------------------------------------------------------------------- 15 | // 16 | // 2.0.0 17 | // - Updated to no longer require unsafe or fixed code. 18 | // - Removed unneeded methods and code. 19 | // 20 | // 1.0.0 21 | // - First version written and release. 22 | // 23 | // [ CREDITS ] ---------------------------------------------------------------------------- 24 | // 25 | // sigScan is based on the FindPattern code written by 26 | // dom1n1k and Patrick at GameDeception.net 27 | // 28 | // Full credit to them for the purpose of this code. I, atom0s, simply 29 | // take credit for converting it to C#. 30 | // 31 | // [ USAGE ] ------------------------------------------------------------------------------ 32 | // 33 | // Examples: 34 | // 35 | // SigScan _sigScan = new SigScan(); 36 | // _sigScan.Process = someProc; 37 | // _sigScan.Address = new IntPtr(0x123456); 38 | // _sigScan.Size = 0x1000; 39 | // IntPtr pAddr = _sigScan.FindPattern(new byte[]{ 0xFF, 0xFF, 0xFF, 0xFF, 0x51, 0x55, 0xFC, 0x11 }, "xxxx?xx?", 12); 40 | // 41 | // SigScan _sigScan = new SigScan(someProc, new IntPtr(0x123456), 0x1000); 42 | // IntPtr pAddr = _sigScan.FindPattern(new byte[]{ 0xFF, 0xFF, 0xFF, 0xFF, 0x51, 0x55, 0xFC, 0x11 }, "xxxx?xx?", 12); 43 | // 44 | // ---------------------------------------------------------------------------------------- 45 | namespace OsuRTDataProvider.Memory 46 | { 47 | internal class SigScan 48 | { 49 | private class MemoryRegion 50 | { 51 | public IntPtr AllocationBase { get; set; } 52 | public IntPtr BaseAddress { get; set; } 53 | public ulong RegionSize { get; set; } 54 | public byte[] DumpedRegion { get; set; } 55 | } 56 | 57 | /// 58 | /// m_vProcess 59 | /// 60 | /// The process we want to read the memory of. 61 | /// 62 | private Process m_vProcess; 63 | 64 | /// 65 | /// m_vAddress 66 | /// 67 | /// The starting address we want to begin reading at. 68 | /// 69 | 70 | /// 71 | /// m_vSize 72 | /// 73 | /// The number of bytes we wish to read from the process. 74 | /// 75 | 76 | #region "sigScan Class Construction" 77 | 78 | /// 79 | /// SigScan 80 | /// 81 | /// Overloaded class constructor that sets the class 82 | /// properties during construction. 83 | /// 84 | /// The process to dump the memory from. 85 | /// The started address to begin the dump. 86 | /// The size of the dump. 87 | public SigScan(Process proc) 88 | { 89 | this.m_vProcess = proc; 90 | //InitMemoryRegionInfo(); 91 | } 92 | 93 | #endregion "sigScan Class Construction" 94 | 95 | public void Reload() 96 | { 97 | ResetRegion(); 98 | InitMemoryRegionInfo(); 99 | } 100 | 101 | private List m_memoryRegionList = new List(); 102 | private const int PROCESS_QUERY_INFORMATION = 0x0400; 103 | private const int MEM_COMMIT = 0x00001000; 104 | private const int PAGE_READWRITE = 0x04; 105 | private const int PROCESS_WM_READ = 0x0010; 106 | 107 | private unsafe void InitMemoryRegionInfo() 108 | { 109 | SYSTEM_INFO sys_info; 110 | //Get the maximum and minimum addresses of the process. 111 | GetSystemInfo(out sys_info); 112 | IntPtr proc_min_address = sys_info.minimumApplicationAddress; 113 | IntPtr proc_max_address = sys_info.maximumApplicationAddress; 114 | 115 | byte* current_address = (byte*)proc_min_address.ToPointer(); 116 | byte* lproc_max_address = (byte*)proc_max_address.ToPointer(); 117 | 118 | IntPtr handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_WM_READ, false, m_vProcess.Id); 119 | 120 | if (handle == IntPtr.Zero) 121 | { 122 | Logger.Error($"Error Code:0x{Marshal.GetLastWin32Error():X8}"); 123 | return; 124 | } 125 | 126 | MemoryBasicInformation mem_basic_info = new MemoryBasicInformation(); 127 | 128 | while (current_address < lproc_max_address) 129 | { 130 | //Query the current memory page information. 131 | bool ok = QueryMemoryBasicInformation(handle, (IntPtr)current_address, ref mem_basic_info); 132 | 133 | if (!ok) 134 | { 135 | Logger.Error($"Error Code:0x{Marshal.GetLastWin32Error():X8}"); 136 | break; 137 | } 138 | 139 | //Dump JIT code 140 | if ((mem_basic_info.Protect & AllocationProtect.PAGE_EXECUTE_READWRITE)>0 && mem_basic_info.State == MEM_COMMIT) 141 | { 142 | var region = new MemoryRegion() 143 | { 144 | BaseAddress = mem_basic_info.BaseAddress, 145 | AllocationBase = mem_basic_info.AllocationBase, 146 | RegionSize = mem_basic_info.RegionSize 147 | }; 148 | m_memoryRegionList.Add(region); 149 | } 150 | 151 | //if (Setting.DebugMode) 152 | //{ 153 | // LogHelper.EncryptLog($"BaseAddress: 0x{mem_basic_info.BaseAddress:X8} RegionSize: 0x{mem_basic_info.RegionSize:X8} AllocationBase: 0x{mem_basic_info.AllocationBase:X8} Protect: {mem_basic_info.Protect} Commit: {mem_basic_info.State==MEM_COMMIT}(0x{mem_basic_info.State:X8})"); 154 | //} 155 | 156 | current_address += mem_basic_info.RegionSize; 157 | } 158 | 159 | CloseHandle(handle); 160 | 161 | if(m_memoryRegionList.Count==0) 162 | { 163 | Logger.Error($"Error:List is Empty"); 164 | } 165 | } 166 | 167 | #region "sigScan Class Private Methods" 168 | 169 | /// 170 | /// DumpMemory 171 | /// 172 | /// Internal memory dump function that uses the set class 173 | /// properties to dump a memory region. 174 | /// 175 | /// Boolean based on RPM results and valid properties. 176 | private bool DumpMemory() 177 | { 178 | try 179 | { 180 | // Checks to ensure we have valid data. 181 | if (this.m_vProcess == null) 182 | return false; 183 | if (this.m_vProcess.HasExited == true) 184 | return false; 185 | 186 | // Create the region space to dump into. 187 | foreach (var region in m_memoryRegionList) 188 | { 189 | if (region.DumpedRegion != null) continue; 190 | 191 | region.DumpedRegion = new byte[region.RegionSize]; 192 | 193 | bool bReturn = false; 194 | int nBytesRead = 0; 195 | 196 | // Dump the memory. 197 | bReturn = ReadProcessMemory( 198 | this.m_vProcess.Handle, region.BaseAddress, region.DumpedRegion, (uint)region.RegionSize, out nBytesRead 199 | ); 200 | 201 | // Validation checks. 202 | if (bReturn == false || nBytesRead != (int)region.RegionSize) 203 | return false; 204 | } 205 | return true; 206 | } 207 | catch (Exception ex) 208 | { 209 | Logger.Error($":{ex.Message}"); 210 | return false; 211 | } 212 | } 213 | 214 | /// 215 | /// MaskCheck 216 | /// 217 | /// Compares the current pattern byte to the current memory dump 218 | /// byte to check for a match. Uses wildcards to skip bytes that 219 | /// are deemed unneeded in the compares. 220 | /// 221 | /// Offset in the dump to start at. 222 | /// Pattern to scan for. 223 | /// Mask to compare against. 224 | /// Boolean depending on if the pattern was found. 225 | private bool MaskCheck(MemoryRegion region, int nOffset, byte[] btPattern, string strMask) 226 | { 227 | // Loop the pattern and compare to the mask and dump. 228 | for (int x = 0; x < btPattern.Length && (ulong)(nOffset + x) < region.RegionSize; x++) 229 | { 230 | // If the mask char is a wildcard, just continue. 231 | if (strMask[x] == '?') 232 | continue; 233 | 234 | // If the mask char is not a wildcard, ensure a match is made in the pattern. 235 | if ((strMask[x] == 'x') && (btPattern[x] != region.DumpedRegion[nOffset + x])) 236 | return false; 237 | } 238 | 239 | // The loop was successful so we found the pattern. 240 | return true; 241 | } 242 | 243 | #endregion "sigScan Class Private Methods" 244 | 245 | #region "sigScan Class Public Methods" 246 | 247 | /// 248 | /// FindPattern 249 | /// 250 | /// Attempts to locate the given pattern inside the dumped memory region 251 | /// compared against the given mask. If the pattern is found, the offset 252 | /// is added to the located address and returned to the user. 253 | /// 254 | /// Byte pattern to look for in the dumped region. 255 | /// The mask string to compare against. 256 | /// The offset added to the result address. 257 | /// IntPtr - zero if not found, address if found. 258 | public IntPtr FindPattern(byte[] btPattern, string strMask, int nOffset) 259 | { 260 | try 261 | { 262 | // Dump the memory region if we have not dumped it yet. 263 | 264 | if (!this.DumpMemory()) 265 | return IntPtr.Zero; 266 | 267 | // Ensure the mask and pattern lengths match. 268 | if (strMask.Length != btPattern.Length) 269 | return IntPtr.Zero; 270 | 271 | // Loop the region and look for the pattern. 272 | foreach (var region in m_memoryRegionList) 273 | { 274 | for (int x = 0; x < region.DumpedRegion.Length; x++) 275 | { 276 | if (this.MaskCheck(region, x, btPattern, strMask)) 277 | { 278 | // The pattern was found, return it. 279 | return new IntPtr((int)region.BaseAddress + (x + nOffset)); 280 | } 281 | } 282 | } 283 | // Pattern was not found. 284 | return IntPtr.Zero; 285 | } 286 | catch (Exception ex) 287 | { 288 | Logger.Error($":{ex.Message}"); 289 | return IntPtr.Zero; 290 | } 291 | } 292 | 293 | /// 294 | /// ResetRegion 295 | /// 296 | /// Resets the memory dump array to nothing to allow 297 | /// the class to redump the memory. 298 | /// 299 | public void ResetRegion() 300 | { 301 | m_memoryRegionList.Clear(); 302 | } 303 | 304 | #endregion "sigScan Class Public Methods" 305 | 306 | #region "sigScan Class Properties" 307 | 308 | private bool isX64() 309 | { 310 | return IntPtr.Size == 8; 311 | } 312 | 313 | private bool QueryMemoryBasicInformation(IntPtr handle, IntPtr current_address, ref MemoryBasicInformation memoryBasicInformation) 314 | { 315 | if (isX64()) 316 | { 317 | MEMORY_BASIC_INFORMATION_X64 mem_basic_info = new MEMORY_BASIC_INFORMATION_X64(); 318 | int mem_info_size = Marshal.SizeOf(); 319 | int size = VirtualQueryEx_X64(handle, current_address, out mem_basic_info, (uint)mem_info_size); 320 | 321 | if (size != mem_info_size) 322 | { 323 | Logger.Error($"(X64)Error Code:0x{Marshal.GetLastWin32Error():X8}"); 324 | return false; 325 | } 326 | 327 | memoryBasicInformation.RegionSize = mem_basic_info.RegionSize; 328 | memoryBasicInformation.BaseAddress = mem_basic_info.BaseAddress; 329 | memoryBasicInformation.AllocationProtect = mem_basic_info.AllocationProtect; 330 | memoryBasicInformation.AllocationBase = mem_basic_info.AllocationBase; 331 | memoryBasicInformation.Type = mem_basic_info.Type; 332 | memoryBasicInformation.State = mem_basic_info.State; 333 | memoryBasicInformation.Protect = mem_basic_info.Protect; 334 | return true; 335 | } 336 | else 337 | { 338 | MEMORY_BASIC_INFORMATION_X86 mem_basic_info = new MEMORY_BASIC_INFORMATION_X86(); 339 | int mem_info_size = Marshal.SizeOf(); 340 | int size = VirtualQueryEx_X86(handle, current_address, out mem_basic_info, (uint)mem_info_size); 341 | 342 | if (size != mem_info_size) 343 | { 344 | Logger.Error($"(X86)Error Code:0x{Marshal.GetLastWin32Error():X8}"); 345 | return false; 346 | } 347 | 348 | memoryBasicInformation.RegionSize = mem_basic_info.RegionSize; 349 | memoryBasicInformation.BaseAddress = mem_basic_info.BaseAddress; 350 | memoryBasicInformation.AllocationProtect = mem_basic_info.AllocationProtect; 351 | memoryBasicInformation.AllocationBase = mem_basic_info.AllocationBase; 352 | memoryBasicInformation.Type = mem_basic_info.Type; 353 | memoryBasicInformation.State = mem_basic_info.State; 354 | memoryBasicInformation.Protect = mem_basic_info.Protect; 355 | return true; 356 | } 357 | } 358 | 359 | public Process Process 360 | { 361 | get { return this.m_vProcess; } 362 | set { this.m_vProcess = value; } 363 | } 364 | 365 | #endregion "sigScan Class Properties" 366 | 367 | #region PInvoke 368 | 369 | private struct MemoryBasicInformation 370 | { 371 | public IntPtr BaseAddress; 372 | public IntPtr AllocationBase; 373 | public uint AllocationProtect; 374 | public ulong RegionSize; 375 | public uint State; 376 | public AllocationProtect Protect; 377 | public uint Type; 378 | } 379 | 380 | 381 | [StructLayout(LayoutKind.Sequential)] 382 | private struct MEMORY_BASIC_INFORMATION_X86 383 | { 384 | public IntPtr BaseAddress; 385 | public IntPtr AllocationBase; 386 | public uint AllocationProtect; 387 | public uint RegionSize; 388 | public uint State; 389 | public AllocationProtect Protect; 390 | public uint Type; 391 | } 392 | 393 | [StructLayout(LayoutKind.Sequential, Pack = 16)] 394 | private struct MEMORY_BASIC_INFORMATION_X64 395 | { 396 | public IntPtr BaseAddress; 397 | public IntPtr AllocationBase; 398 | public uint AllocationProtect; 399 | public uint __alignment1; 400 | public ulong RegionSize; 401 | public uint State; 402 | public AllocationProtect Protect; 403 | public uint Type; 404 | public uint __alignment2; 405 | } 406 | public enum AllocationProtect : uint 407 | { 408 | PAGE_EXECUTE = 0x00000010, 409 | PAGE_EXECUTE_READ = 0x00000020, 410 | PAGE_EXECUTE_READWRITE = 0x00000040, 411 | PAGE_EXECUTE_WRITECOPY = 0x00000080, 412 | PAGE_NOACCESS = 0x00000001, 413 | PAGE_READONLY = 0x00000002, 414 | PAGE_READWRITE = 0x00000004, 415 | PAGE_WRITECOPY = 0x00000008, 416 | PAGE_GUARD = 0x00000100, 417 | PAGE_NOCACHE = 0x00000200, 418 | PAGE_WRITECOMBINE = 0x00000400 419 | } 420 | 421 | [StructLayout(LayoutKind.Sequential)] 422 | public struct SYSTEM_INFO 423 | { 424 | public ushort processorArchitecture; 425 | private ushort reserved; 426 | public uint pageSize; 427 | public IntPtr minimumApplicationAddress; 428 | public IntPtr maximumApplicationAddress; 429 | public IntPtr activeProcessorMask; 430 | public uint numberOfProcessors; 431 | public uint processorType; 432 | public uint allocationGranularity; 433 | public ushort processorLevel; 434 | public ushort processorRevision; 435 | } 436 | 437 | /// 438 | /// ReadProcessMemory 439 | /// 440 | /// API import definition for ReadProcessMemory. 441 | /// 442 | /// Handle to the process we want to read from. 443 | /// The base address to start reading from. 444 | /// The return buffer to write the read data to. 445 | /// The size of data we wish to read. 446 | /// The number of bytes successfully read. 447 | /// 448 | [DllImport("kernel32.dll", SetLastError = true)] 449 | public static extern bool ReadProcessMemory( 450 | IntPtr hProcess, 451 | IntPtr lpBaseAddress, 452 | [Out] byte[] lpBuffer, 453 | uint dwSize, 454 | out int lpNumberOfBytesRead 455 | ); 456 | 457 | [DllImport("kernel32.dll", SetLastError = true, EntryPoint = "VirtualQueryEx")] 458 | private static extern int VirtualQueryEx_X86(IntPtr hProcess, IntPtr lpAddress, out MEMORY_BASIC_INFORMATION_X86 lpBuffer, uint dwLength); 459 | 460 | [DllImport("kernel32.dll", SetLastError = true, EntryPoint = "VirtualQueryEx")] 461 | private static extern int VirtualQueryEx_X64(IntPtr hProcess, IntPtr lpAddress, out MEMORY_BASIC_INFORMATION_X64 lpBuffer, uint dwLength); 462 | 463 | [DllImport("kernel32.dll", SetLastError = true)] 464 | private static extern void GetSystemInfo(out SYSTEM_INFO lpSystemInfo); 465 | 466 | [DllImport("kernel32.dll", SetLastError = true)] 467 | public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId); 468 | 469 | [DllImport("kernel32.dll", SetLastError = true)] 470 | public static extern bool CloseHandle(IntPtr hObject); 471 | 472 | #endregion PInvoke 473 | } 474 | } -------------------------------------------------------------------------------- /Mods/ModsInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace OsuRTDataProvider.Mods 6 | { 7 | public struct ModsInfo 8 | { 9 | [Flags] 10 | public enum Mods : uint 11 | { 12 | NoMod = 0u, 13 | None = 0u, 14 | NoFail = 1u << 0, 15 | Easy = 1u << 1, 16 | TouchDevice = 1u << 2, 17 | Hidden = 1u << 3, 18 | HardRock = 1u << 4, 19 | SuddenDeath = 1u << 5, 20 | DoubleTime = 1u << 6, 21 | Relax = 1u << 7, 22 | HalfTime = 1u << 8, 23 | Nightcore = 1u << 9, 24 | Flashlight = 1u << 10, 25 | Autoplay = 1u << 11, 26 | SpunOut = 1u << 12, 27 | AutoPilot = 1u << 13, 28 | Perfect = 1u << 14, 29 | Key4 = 1u << 15, 30 | Key5 = 1u << 16, 31 | Key6 = 1u << 17, 32 | Key7 = 1u << 18, 33 | Key8 = 1u << 19, 34 | FadeIn = 1u << 20, 35 | Random = 1u << 21, 36 | Cinema = 1u << 22, 37 | Target = 1u << 23, 38 | Key9 = 1u << 24, 39 | KeyCoop = 1u << 25, 40 | Key1 = 1u << 26, 41 | Key3 = 1u << 27, 42 | Key2 = 1u << 28, 43 | ScoreV2 = 1u << 29, 44 | Mirror = 1u << 30, 45 | Unknown = 0xFFFFFFFFu 46 | } 47 | 48 | private static Dictionary s_name_to_sname = new Dictionary 49 | { 50 | ["NoMod"] = "", 51 | ["None"] = "", 52 | ["NoFail"] = "NF", 53 | ["Easy"] = "EZ", 54 | ["TouchDevice"] = "TD", 55 | ["Hidden"] = "HD", 56 | ["HardRock"] = "HR", 57 | ["SuddenDeath"] = "SD", 58 | ["DoubleTime"] = "DT", 59 | ["Relax"] = "RL", 60 | ["HalfTime"] = "HT", 61 | ["Nightcore"] = "NC", 62 | ["Flashlight"] = "FL", 63 | ["Autoplay"] = "Auto", 64 | ["SpunOut"] = "SO", 65 | ["AutoPilot"] = "AP", 66 | ["Perfect"] = "PF", 67 | ["Key1"] = "1K", 68 | ["Key2"] = "2K", 69 | ["Key3"] = "3K", 70 | ["Key4"] = "4K", 71 | ["Key5"] = "5K", 72 | ["Key6"] = "6K", 73 | ["Key7"] = "7K", 74 | ["Key8"] = "8K", 75 | ["Key9"] = "9K", 76 | ["KeyCoop"] = "Co-op", 77 | ["FadeIn"] = "FI", 78 | ["Random"] = "RD", 79 | ["Cinema"] = "CN", 80 | ["Target"] = "TP", 81 | ["ScoreV2"] = "V2", 82 | ["Mirror"] = "MR", 83 | ["Unknown"] = "Unknown" 84 | }; 85 | 86 | static public ModsInfo Empty 87 | { 88 | get 89 | { 90 | ModsInfo m; 91 | m.m_mod = Mods.Unknown; 92 | m.m_time_rate = 1.0; 93 | return m; 94 | } 95 | } 96 | 97 | private Mods m_mod; 98 | 99 | /// 100 | /// Get Mods 101 | /// 102 | public Mods Mod 103 | { 104 | set 105 | { 106 | if ((value & Mods.Nightcore) == Mods.Nightcore) 107 | value &= ~Mods.DoubleTime; 108 | else if ((value & Mods.Cinema) == Mods.Cinema) 109 | value &= ~Mods.Autoplay; 110 | else if ((value & Mods.Perfect) == Mods.Perfect) 111 | value &= ~Mods.SuddenDeath; 112 | 113 | if ((value & Mods.Nightcore) > 0 || (value & Mods.DoubleTime) > 0) 114 | m_time_rate = 1.5; 115 | else if ((value & Mods.HalfTime) > 0) 116 | m_time_rate = 0.75; 117 | else 118 | m_time_rate = 1.0; 119 | 120 | m_mod = value; 121 | } 122 | get => m_mod; 123 | } 124 | 125 | private double m_time_rate; 126 | public double TimeRate => m_time_rate; 127 | 128 | private static readonly List s_invaild_mods_mask = new List { 129 | Mods.Easy|Mods.HardRock, 130 | Mods.HalfTime|Mods.DoubleTime, 131 | Mods.SuddenDeath|Mods.AutoPilot, 132 | Mods.AutoPilot|Mods.Relax, 133 | Mods.NoFail|Mods.SuddenDeath, 134 | Mods.NoFail|Mods.Perfect, 135 | Mods.NoFail|Mods.Relax, 136 | Mods.NoFail|Mods.AutoPilot, 137 | Mods.Relax|Mods.SuddenDeath, 138 | Mods.SpunOut|Mods.AutoPilot, 139 | Mods.Key1 | Mods.Key3, 140 | Mods.Key1 | Mods.Key4, 141 | Mods.Key1 | Mods.Key5, 142 | Mods.Key1 | Mods.Key6, 143 | Mods.Key1 | Mods.Key7, 144 | Mods.Key1 | Mods.Key8, 145 | Mods.Key1 | Mods.Key9, 146 | 147 | Mods.Key2 | Mods.Key3, 148 | Mods.Key2 | Mods.Key4, 149 | Mods.Key2 | Mods.Key5, 150 | Mods.Key2 | Mods.Key6, 151 | Mods.Key2 | Mods.Key7, 152 | Mods.Key2 | Mods.Key8, 153 | Mods.Key2 | Mods.Key9, 154 | 155 | Mods.Key3 | Mods.Key4, 156 | Mods.Key3 | Mods.Key5, 157 | Mods.Key3 | Mods.Key6, 158 | Mods.Key3 | Mods.Key7, 159 | Mods.Key3 | Mods.Key8, 160 | Mods.Key3 | Mods.Key9, 161 | 162 | Mods.Key4 | Mods.Key5, 163 | Mods.Key4 | Mods.Key6, 164 | Mods.Key4 | Mods.Key7, 165 | Mods.Key4 | Mods.Key8, 166 | Mods.Key4 | Mods.Key9, 167 | 168 | Mods.Key5 | Mods.Key6, 169 | Mods.Key5 | Mods.Key7, 170 | Mods.Key5 | Mods.Key8, 171 | Mods.Key5 | Mods.Key9, 172 | 173 | Mods.Key6 | Mods.Key7, 174 | Mods.Key6 | Mods.Key8, 175 | Mods.Key6 | Mods.Key9, 176 | 177 | Mods.Key7 | Mods.Key8, 178 | Mods.Key7 | Mods.Key9, 179 | 180 | Mods.Key8 | Mods.Key9, 181 | }; 182 | 183 | public static bool VaildMods(ModsInfo mods) 184 | { 185 | foreach(var mask in s_invaild_mods_mask) 186 | { 187 | if ((mask & mods.Mod) == mask) 188 | return false; 189 | } 190 | return true; 191 | } 192 | 193 | /// 194 | /// Get Mods Name 195 | /// 196 | public string Name 197 | { 198 | get 199 | { 200 | var mods_str = Mod.ToString().Replace(" ", string.Empty); 201 | mods_str = mods_str.Replace("None", "NoMod"); 202 | if (mods_str.Contains("NoMod,")) 203 | { 204 | mods_str = mods_str.Replace("NoMod,", ""); 205 | } 206 | return mods_str; 207 | } 208 | } 209 | 210 | /// 211 | /// Get Short Mods Name 212 | /// 213 | public string ShortName 214 | { 215 | get 216 | { 217 | string mods_str = Name; 218 | string[] mods_arr = mods_str.Replace(" ", string.Empty).Split(','); 219 | StringBuilder b = new StringBuilder(128); 220 | 221 | foreach (var str in mods_arr) 222 | { 223 | if (s_name_to_sname.ContainsKey(str)) 224 | b.Append(s_name_to_sname[str]); 225 | else return "Error"; 226 | b.Append(','); 227 | } 228 | return b.Remove(b.Length - 1, 1).ToString().Trim(','); 229 | } 230 | } 231 | 232 | public bool HasMod(Mods mods) 233 | { 234 | return (m_mod & mods) > 0; 235 | } 236 | 237 | public override string ToString() 238 | { 239 | return ShortName; 240 | } 241 | 242 | public override bool Equals(object obj) 243 | { 244 | if (!(obj is ModsInfo)) 245 | { 246 | return false; 247 | } 248 | 249 | var info = (ModsInfo)obj; 250 | return m_mod == info.m_mod; 251 | } 252 | 253 | public override int GetHashCode() 254 | { 255 | var hashCode = -801518429; 256 | hashCode = hashCode * -1521134295 + base.GetHashCode(); 257 | hashCode = hashCode * -1521134295 + m_mod.GetHashCode(); 258 | return hashCode; 259 | } 260 | 261 | public static bool operator !=(ModsInfo a, ModsInfo b) 262 | { 263 | return a.Mod != b.Mod; 264 | } 265 | 266 | public static bool operator ==(ModsInfo a, ModsInfo b) 267 | { 268 | return a.Mod == b.Mod; 269 | } 270 | 271 | public static bool operator !=(ModsInfo a, Mods b) 272 | { 273 | return a.Mod != b; 274 | } 275 | 276 | public static bool operator ==(ModsInfo a, Mods b) 277 | { 278 | return a.Mod == b; 279 | } 280 | } 281 | } -------------------------------------------------------------------------------- /OsuRTDataProvider.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {D518DC59-CC9A-4886-839F-7691D5EFAE56} 8 | Library 9 | Properties 10 | OsuRTDataProvider 11 | OsuRTDataProvider 12 | v4.5.2 13 | 512 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\Plugins\ 21 | TRACE;DEBUG 22 | prompt 23 | 4 24 | AnyCPU 25 | false 26 | true 27 | false 28 | 29 | 30 | none 31 | true 32 | bin\Release\ 33 | TRACE 34 | prompt 35 | 4 36 | AnyCPU 37 | true 38 | 39 | 40 | true 41 | bin\x86\Debug\ 42 | TRACE;DEBUG;X86 43 | full 44 | x86 45 | prompt 46 | MinimumRecommendedRules.ruleset 47 | 48 | 49 | bin\x86\Release\ 50 | TRACE 51 | true 52 | pdbonly 53 | x86 54 | prompt 55 | MinimumRecommendedRules.ruleset 56 | 57 | 58 | true 59 | bin\x64\Debug\ 60 | TRACE;DEBUG;X64 61 | full 62 | x64 63 | prompt 64 | MinimumRecommendedRules.ruleset 65 | 66 | 67 | bin\x64\Release\ 68 | TRACE 69 | x64 70 | prompt 71 | MinimumRecommendedRules.ruleset 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | {FBD514C2-2830-479E-B050-D1C383028456} 116 | Sync 117 | False 118 | 119 | 120 | 121 | 122 | copy /y $(TargetPath) $(SolutionDir)\Sync\bin\$(ConfigurationName)\Plugins 123 | 124 | -------------------------------------------------------------------------------- /OsuRTDataProviderPlugin.cs: -------------------------------------------------------------------------------- 1 | using OsuRTDataProvider.BeatmapInfo; 2 | using OsuRTDataProvider.Helper; 3 | using OsuRTDataProvider.Listen; 4 | using OsuRTDataProvider.Mods; 5 | using Sync; 6 | using Sync.Plugins; 7 | using Sync.Tools; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Threading.Tasks; 13 | using static OsuRTDataProvider.Listen.OsuListenerManager; 14 | 15 | namespace OsuRTDataProvider 16 | { 17 | [SyncPluginID("7216787b-507b-4eef-96fb-e993722acf2e", VERSION)] 18 | public class OsuRTDataProviderPlugin : Plugin 19 | { 20 | public const string PLUGIN_NAME = "OsuRTDataProvider"; 21 | public const string PLUGIN_AUTHOR = "KedamaOvO"; 22 | public const string VERSION = "1.7.0"; 23 | 24 | private PluginConfigurationManager m_config_manager; 25 | 26 | private OsuListenerManager[] m_listener_managers = new OsuListenerManager[16]; 27 | private int m_listener_managers_count = 0; 28 | 29 | public bool ModsChangedAtListening => Setting.EnableModsChangedAtListening; 30 | 31 | /// 32 | /// If EnableTourneyMode = false in config.ini, return 0. 33 | /// If EnableTourneyMode = true in config.ini, return TeamSize * 2. 34 | /// 35 | public int TourneyListenerManagersCount { get => Setting.EnableTourneyMode ? m_listener_managers_count : 0; } 36 | 37 | /// 38 | /// return a ListenerManager. 39 | /// 40 | public OsuListenerManager ListenerManager { get => m_listener_managers[0]; } 41 | 42 | /// 43 | /// If EnableTourneyMode = false in config.ini, return null. 44 | /// If EnableTourneyMode = true in config.ini, return all ListenerManagers. 45 | /// 46 | public OsuListenerManager[] TourneyListenerManagers { get => Setting.EnableTourneyMode ? m_listener_managers : null; } 47 | 48 | public OsuRTDataProviderPlugin() : base(PLUGIN_NAME, PLUGIN_AUTHOR) 49 | { 50 | I18n.Instance.ApplyLanguage(new DefaultLanguage()); 51 | m_config_manager = new PluginConfigurationManager(this); 52 | m_config_manager.AddItem(new SettingIni()); 53 | 54 | if (Setting.DebugMode) 55 | { 56 | base.EventBus.BindEvent((e) => 57 | HardwareInformationHelper.PrintHardwareInformation()); 58 | } 59 | 60 | base.EventBus.BindEvent(InitCommand); 61 | base.EventBus.BindEvent((e) => { 62 | Logger.Info(string.Format(DefaultLanguage.LANG_TOURNEY_HINT, Setting.EnableTourneyMode)); 63 | Task.Run(()=>UpdateChecker.CheckUpdate()); 64 | }); 65 | 66 | try 67 | { 68 | File.Delete("OsuRTDataProviderRelease.dll"); 69 | } 70 | catch (Exception) { } 71 | } 72 | 73 | public override void OnEnable() 74 | { 75 | Sync.Tools.IO.CurrentIO.WriteColor(PLUGIN_NAME + " By " + PLUGIN_AUTHOR, ConsoleColor.DarkCyan); 76 | 77 | if (Setting.EnableTourneyMode) 78 | { 79 | m_listener_managers_count = Setting.TeamSize * 2; 80 | for (int i = 0; i < m_listener_managers_count; i++) 81 | InitTourneyManager(i); 82 | } 83 | else 84 | { 85 | InitManager(); 86 | } 87 | 88 | DebugOutput(Setting.DebugMode, true); 89 | } 90 | 91 | private void InitTourneyManager(int id) 92 | { 93 | m_listener_managers[id] = new OsuListenerManager(true, id); 94 | m_listener_managers[id].Start(); 95 | } 96 | 97 | private void InitManager() 98 | { 99 | m_listener_managers[0] = new OsuListenerManager(); 100 | m_listener_managers[0].Start(); 101 | } 102 | 103 | private void InitCommand(PluginEvents.InitCommandEvent @e) 104 | { 105 | @e.Commands.Dispatch.bind("ortdp", (args) => 106 | { 107 | if(args.Count >= 1) 108 | { 109 | if(args[0] == "releases") 110 | { 111 | System.Diagnostics.Process.Start("https://github.com/OsuSync/OsuRTDataProvider/releases"); 112 | } 113 | } 114 | 115 | if (args.Count >= 2) 116 | { 117 | if (args[0] == "debug") 118 | { 119 | if (bool.TryParse(args[1], out bool f)) 120 | { 121 | DebugOutput(f); 122 | Logger.Info($"Debug mode = {Setting.DebugMode}"); 123 | } 124 | } 125 | return true; 126 | } 127 | return false; 128 | }, "OsuRTDataProvider control panel"); 129 | } 130 | 131 | private void DebugOutput(bool enable, bool first = false) 132 | { 133 | if (!first && Setting.DebugMode == enable) return; 134 | 135 | if (Setting.EnableTourneyMode) 136 | { 137 | for (int i = 0; i < TourneyListenerManagersCount; i++) 138 | { 139 | int id = i; 140 | void OnTourneyStatusChanged(OsuStatus l, OsuStatus c)=> 141 | Logger.Info($"[{id}]Current Game Status:{c}"); 142 | void OnTourneyModsChanged(ModsInfo m)=> 143 | Logger.Info($"[{id}]Mods:{m}(0x{(uint)m.Mod:X8})"); 144 | void OnTourneyModeChanged(OsuPlayMode last, OsuPlayMode mode)=> 145 | Logger.Info($"[{id}]Mode:{mode}"); 146 | void OnTourneyBeatmapChanged(Beatmap map) => 147 | Logger.Info($"[{id}]Beatmap: {map.Artist} - {map.Title}[{map.Difficulty}]({map.BeatmapSetID},{map.BeatmapID},{map.FilenameFull})"); 148 | void OnTourneyPlayerChanged(string playername) => 149 | Logger.Info($"[{id}]Current Player: {playername}"); 150 | /* 151 | void OnTourneyHitEventsChanged(PlayType playType, List hitEvents) 152 | { 153 | string log = $"[{id}]Play Type: {playType}, end time: {(hitEvents.Count == 0 ? -1 : hitEvents[hitEvents.Count - 1].TimeStamp)}, count: {hitEvents.Count}"; 154 | log += $" LastKeysDown:{hitEvents.LastOrDefault()?.KeysDown}"; 155 | Logger.Info(log); 156 | }; 157 | */ 158 | 159 | if (enable) 160 | { 161 | m_listener_managers[i].OnStatusChanged += OnTourneyStatusChanged; 162 | m_listener_managers[i].OnModsChanged += OnTourneyModsChanged; 163 | m_listener_managers[i].OnPlayModeChanged += OnTourneyModeChanged; 164 | m_listener_managers[i].OnBeatmapChanged += OnTourneyBeatmapChanged; 165 | m_listener_managers[i].OnPlayerChanged += OnTourneyPlayerChanged; 166 | //m_listener_managers[i].OnHitEventsChanged += OnTourneyHitEventsChanged; 167 | } 168 | else 169 | { 170 | m_listener_managers[i].OnStatusChanged -= OnTourneyStatusChanged; 171 | m_listener_managers[i].OnModsChanged -= OnTourneyModsChanged; 172 | m_listener_managers[i].OnPlayModeChanged -= OnTourneyModeChanged; 173 | //m_listener_managers[i].OnHitEventsChanged -= OnTourneyHitEventsChanged; 174 | } 175 | } 176 | } 177 | else 178 | { 179 | void OnStatusChanged(OsuStatus l, OsuStatus c) => 180 | Logger.Info($"Current Game Status:{c}"); 181 | void OnModsChanged(ModsInfo m) => 182 | Logger.Info($"Mods:{m}(0x{(uint)m.Mod:X8})"); 183 | void OnModeChanged(OsuPlayMode last, OsuPlayMode mode) => 184 | Logger.Info($"Mode:{mode}"); 185 | void OnBeatmapChanged(Beatmap map) => 186 | Logger.Info($"Beatmap: {map.Artist} - {map.Title}[{map.Difficulty}]({map.BeatmapSetID},{map.BeatmapID},{map.FilenameFull})"); 187 | void OnPlayerChanged(string playername) => 188 | Logger.Info($"Current Player: {playername}"); 189 | /* 190 | void OnHitEventsChanged(PlayType playType, List hitEvents) 191 | { 192 | string log = $"Play Type: {playType}, end time: {(hitEvents.Count == 0 ? -1 : hitEvents[hitEvents.Count - 1].TimeStamp)}, count: {hitEvents.Count}"; 193 | log += $" LastKeysDown:{hitEvents.LastOrDefault()?.KeysDown}"; 194 | Logger.Info(log); 195 | }; 196 | */ 197 | 198 | if (enable) 199 | { 200 | m_listener_managers[0].OnStatusChanged += OnStatusChanged; 201 | m_listener_managers[0].OnModsChanged += OnModsChanged; 202 | m_listener_managers[0].OnPlayModeChanged += OnModeChanged; 203 | m_listener_managers[0].OnBeatmapChanged += OnBeatmapChanged; 204 | m_listener_managers[0].OnPlayerChanged += OnPlayerChanged; 205 | //m_listener_managers[0].OnHitEventsChanged += OnHitEventsChanged; 206 | } 207 | else 208 | { 209 | m_listener_managers[0].OnStatusChanged -= OnStatusChanged; 210 | m_listener_managers[0].OnModsChanged -= OnModsChanged; 211 | m_listener_managers[0].OnPlayModeChanged -= OnModeChanged; 212 | m_listener_managers[0].OnBeatmapChanged -= OnBeatmapChanged; 213 | m_listener_managers[0].OnPlayerChanged -= OnPlayerChanged; 214 | //m_listener_managers[0].OnHitEventsChanged -= OnHitEventsChanged; 215 | } 216 | } 217 | 218 | Setting.DebugMode = enable; 219 | } 220 | 221 | public override void OnDisable() 222 | { 223 | int size = Setting.EnableTourneyMode ? TourneyListenerManagersCount : 1; 224 | for (int i = 0; i < size; i++) 225 | { 226 | m_listener_managers[i].Stop(); 227 | } 228 | } 229 | 230 | public override void OnExit() 231 | { 232 | OnDisable(); 233 | } 234 | } 235 | } -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // 有关程序集的一般信息由以下 6 | // 控制。更改这些特性值可修改 7 | // 与程序集关联的信息。 8 | [assembly: AssemblyTitle("OsuRTDataProvider")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("OsuRTDataProvider")] 13 | [assembly: AssemblyCopyright("Copyright © 2017")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // 将 ComVisible 设置为 false 会使此程序集中的类型 18 | //对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型 19 | //请将此类型的 ComVisible 特性设置为 true。 20 | [assembly: ComVisible(false)] 21 | 22 | // 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID 23 | [assembly: Guid("d518dc59-cc9a-4886-839f-7691d5efae56")] 24 | 25 | // 程序集的版本信息由下列四个值组成: 26 | // 27 | // 主版本 28 | // 次版本 29 | // 生成号 30 | // 修订号 31 | // 32 | // 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号 33 | //通过使用 "*",如下所示: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion(OsuRTDataProvider.OsuRTDataProviderPlugin.VERSION)] 36 | [assembly: AssemblyFileVersion(OsuRTDataProvider.OsuRTDataProviderPlugin.VERSION)] 37 | -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 | [English](https://github.com/KedamaOvO/OsuRTDataProvider-Release/blob/master/README.md) 2 | # OsuRTDataProvider是什么? 3 | OsuRTDataProvider是一个[OsuSync](https://github.com/Deliay/osuSync)插件,可以实时读取osu!数据. 4 | 它支持OSU!和OSU!Tourney。 5 | 6 | OsuRTDataProvider能实时的从[OSU!](https://osu.ppy.sh)中获取以下内容(只支持正式版): 7 | * BeatmapID 8 | * 游戏状态 9 | * Accuracy 10 | * 生命值 11 | * 当前连击 12 | * 300数量 13 | * 100数量 14 | * 50数量 15 | * Katu数量 16 | * Geki数量 17 | * Miss数量 18 | * Mods 19 | * 播放时间 20 | * 分数 21 | * 游戏模式 22 | 23 | 不同的OSU!客户端版本所需要的ORTDP插件版本也可能不同. 24 | 25 | # 怎么使用(对于普通用户) 26 | 1. 下载 [OsuSync](https://github.com/Deliay/osuSync)。 27 | 2. 下载 [OsuRTDataProvider](https://github.com/KedamaOvO/OsuRTDataProvider-Release/releases)。 28 | 3. 复制OsuRTDataProvider 到 {OsuSync Path}/Plugins 目录下。 29 | 4. 运行 OsuSync。 30 | 5. 安装其他依赖于此插件的插件,比如[实时pp显示插件](https://github.com/OsuSync/RealTimePPDisplayer). 31 | 6. Enjoy! 32 | 33 | 34 | # Config.ini 35 | [OsuRTDataProvider.SettingIni] 36 | 37 | |Setting Name|Default Value|Description| 38 | | ----- | ----- | ----- | 39 | | ListenInterval | 100 | 监听数据的间隔(单位毫秒)。PS:如果太小可能会卡 | 40 | | EnableTourneyMode | False | 启用Tourney模式?(实验性) | 41 | | TeamSize | 1 | Tourney client的队伍大小| 42 | | ForceOsuSongsDirectory | | 强制在指定路径中搜索Beatmap文件夹| 43 | | GameMode | Auto |如果ModeFinder初始化失败. 请手动设置游戏模式(osu,mania,ctb,taiko,auto)| 44 | | DisableProcessNotFoundInformation | False | 隐藏"没有发现osu.exe进程"的消息提示| 45 | | EnableModsChangedAtListening | False | 尝试在非Play状态监听Mods变化| 46 | 47 | # API 48 | #### OsuRTDataProviderPlugin ***class*** 49 | ##### Property 50 | ```csharp 51 | public OsuListenerManager ListenerManager; 52 | 53 | //如果config.ini中的EnableTourneyMode = false,该属性为null. 54 | public OsuListenerManager[] TourneyListenerManagers; 55 | public int TourneyListenerManagersCount; 56 | ``` 57 | 58 | # 如何编译 59 | 1. clone此repo 60 | 2. clone[Sync](https://github.com/OsuSync/Sync) 61 | 3. 引用sync项目进ortdp项目 62 | 4. 编译. 63 | 64 | #### OsuListenerManager ***class*** 65 | ##### Event 66 | ```csharp 67 | public delegate void OnBeatmapChangedEvt(Beatmap map); 68 | public delegate void OnHealthPointChangedEvt(double hp); 69 | public delegate void OnAccuracyChangedEvt(double acc); 70 | public delegate void OnComboChangedEvt(int combo); 71 | public delegate void OnModsChangedEvt(ModsInfo mods); 72 | public delegate void OnPlayingTimeChangedEvt(int ms); 73 | public delegate void OnHitCountChangedEvt(int hit); 74 | public delegate void OnStatusChangedEvt(OsuStatus last_status, OsuStatus status); 75 | public delegate void OnErrorStatisticsChangedEvt(ErrorStatisticsResult result); 76 | public delegate void OnPlayerChangedEvt(string player); 77 | public delegate void OnHitEventsChangedEvt(PlayType playType, List hitEvents); 78 | 79 | /// 80 | /// Playing和Linsten时可用. 81 | /// 如果Beatmap太老, map.ID = -1. 82 | /// 83 | public event OnBeatmapChangedEvt OnBeatmapChanged; 84 | 85 | /// 86 | /// Playing时可用. 87 | /// 88 | public event OnHealthPointChangedEvt OnHealthPointChanged; 89 | 90 | /// 91 | /// Playing时可用. 92 | /// 93 | public event OnAccuracyChangedEvt OnAccuracyChanged; 94 | 95 | /// 96 | /// Playing时可用. 97 | /// 98 | public event OnComboChangedEvt OnComboChanged; 99 | 100 | /// 101 | /// Playing时可用. 102 | /// Playing->Listen时 , mods = ModsInfo.Empty 103 | /// 104 | public event OnModsChangedEvt OnModsChanged; 105 | 106 | /// 107 | /// Playing和Linsten时可用. 108 | /// 109 | public event OnPlayingTimeChangedEvt OnPlayingTimeChanged; 110 | 111 | /// 112 | /// Playing时可用. 113 | /// 114 | public event OnHitCountChangedEvt On300HitChanged; 115 | 116 | /// 117 | /// Playing时可用. 118 | /// 119 | public event OnHitCountChangedEvt On100HitChanged; 120 | 121 | /// 122 | /// Playing时可用. 123 | /// 124 | public event OnHitCountChangedEvt On50HitChanged; 125 | 126 | /// 127 | /// Playing时可用. 128 | /// 129 | public event OnHitCountChangedEvt OnMissHitChanged; 130 | 131 | /// 132 | /// 任何时候可用. 133 | /// 134 | public event OnStatusChangedEvt OnStatusChanged; 135 | 136 | /// 137 | /// Playing可用 138 | /// 错误分析(UnstableRate and Error Hit). 139 | /// 140 | public event OnErrorStatisticsChangedEvt OnErrorStatisticsChanged; 141 | 142 | /// 143 | /// Playing可用 144 | /// 玩家名改变(发生在观看replay时) 145 | /// 146 | public event OnPlayerChangedEvt OnPlayerChanged; 147 | 148 | /// 149 | /// Playing可用 150 | /// 打击事件改变 (https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osr_(file_format)) 151 | /// 152 | public event OnHitEventsChangedEvt OnHitEventsChanged; 153 | ``` 154 | 155 | ##### OsuStatus ***enum*** 156 | ```csharp 157 | public enum OsuStatus 158 | { 159 | NoFoundProcess, 160 | Unkonwn, 161 | Listening, 162 | Playing, 163 | Editing, 164 | Rank 165 | } 166 | ``` 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [中文](https://github.com/KedamaOvO/OsuRTDataProvider-Release/blob/master/README-CN.md) 2 | # What is OsuRTDataProvider? 3 | OsuRTDataProvider is an [OsuSync](https://github.com/Deliay/osuSync) plugin. 4 | it's support OSU! and OSU!Tourney. 5 | 6 | OsuRTDataProvider can be obtained from [OSU!](https://osu.ppy.sh)(Stable Only): 7 | * Beatmap 8 | * Game Status 9 | * Accuracy 10 | * Health Point 11 | * Combo 12 | * 300 Count 13 | * 100 Count 14 | * 50 Count 15 | * Miss Count 16 | * Katu Count 17 | * Geki Count 18 | * Mods 19 | * Play Time 20 | * Score 21 | * Game Mode 22 | 23 | OSU! Clinet Version Requirements: **b20190816 After** 24 | 25 | # How to use? 26 | 1. Download [OsuSync](https://github.com/Deliay/osuSync) 27 | 2. Download [OsuRTDataProvider](https://github.com/KedamaOvO/OsuRTDataProvider-Release/releases). 28 | 3. Copy OsuRTDataProvider to {OsuSync Path}/Plugins. 29 | 4. Run OsuSync. 30 | 31 | # Config.ini 32 | [OsuRTDataProvider.SettingIni] 33 | 34 | |Setting Name|Default Value|Description| 35 | | ----- | ----- | ----- | 36 | | ListenInterval | 100 | Listen data interval(ms). PS: If it is too small may lag | 37 | | EnableTourneyMode | False | Is tourney client?(Experimental)| 38 | | TeamSize | 1 | Tourney client team size| 39 | | ForceOsuSongsDirectory | | Force search for Beatmap from this path| 40 | | GameMode | Auto |If ModeFinder initialization fails. Please manually select the current Mode.| 41 | | DisableProcessNotFoundInformation | False | Hide "Not found osu!.exe process"| 42 | | EnableModsChangedAtListening | False | Try to monitor Mods changes in non-Play status| 43 | 44 | # API 45 | #### OsuRTDataProviderPlugin ***class*** 46 | ##### Property 47 | ```csharp 48 | public OsuListenerManager ListenerManager; 49 | 50 | //If EnableTourneyMode = false in config.ini, return null. 51 | public OsuListenerManager[] TourneyListenerManagers; 52 | public int TourneyListenerManagersCount; 53 | ``` 54 | #### OsuListenerManager ***class*** 55 | ##### Event 56 | ```csharp 57 | public delegate void OnBeatmapChangedEvt(Beatmap map); 58 | public delegate void OnHealthPointChangedEvt(double hp); 59 | public delegate void OnAccuracyChangedEvt(double acc); 60 | public delegate void OnComboChangedEvt(int combo); 61 | public delegate void OnModsChangedEvt(ModsInfo mods); 62 | public delegate void OnPlayingTimeChangedEvt(int ms); 63 | public delegate void OnHitCountChangedEvt(int hit); 64 | public delegate void OnStatusChangedEvt(OsuStatus last_status, OsuStatus status); 65 | public delegate void OnErrorStatisticsChangedEvt(ErrorStatisticsResult result); 66 | public delegate void OnPlayerChangedEvt(string player); 67 | public delegate void OnHitEventsChangedEvt(PlayType playType, List hitEvents); 68 | 69 | /// 70 | /// Available at Playing and Linsten. 71 | /// If too old beatmap, map.ID = -1. 72 | /// 73 | public event OnBeatmapChangedEvt OnBeatmapChanged; 74 | 75 | /// 76 | /// Available at Playing. 77 | /// 78 | public event OnHealthPointChangedEvt OnHealthPointChanged; 79 | 80 | /// 81 | /// Available at Playing. 82 | /// 83 | public event OnAccuracyChangedEvt OnAccuracyChanged; 84 | 85 | /// 86 | /// Available at Playing. 87 | /// 88 | public event OnComboChangedEvt OnComboChanged; 89 | 90 | /// 91 | /// Available at Playing. 92 | /// if OsuStatus turns Listen , mods = ModsInfo.Empty 93 | /// 94 | public event OnModsChangedEvt OnModsChanged; 95 | 96 | /// 97 | /// Available at Playing and Listen. 98 | /// 99 | public event OnPlayingTimeChangedEvt OnPlayingTimeChanged; 100 | 101 | /// 102 | /// Available at Playing. 103 | /// 104 | public event OnHitCountChangedEvt On300HitChanged; 105 | 106 | /// 107 | /// Available at Playing. 108 | /// 109 | public event OnHitCountChangedEvt On100HitChanged; 110 | 111 | /// 112 | /// Available at Playing. 113 | /// 114 | public event OnHitCountChangedEvt On50HitChanged; 115 | 116 | /// 117 | /// Available at Playing. 118 | /// 119 | public event OnHitCountChangedEvt OnMissHitChanged; 120 | 121 | /// 122 | /// Available at Any. 123 | /// 124 | public event OnStatusChangedEvt OnStatusChanged; 125 | 126 | /// 127 | /// Get ErrorStatistics(UnstableRate and Error Hit). 128 | /// 129 | public event OnErrorStatisticsChangedEvt OnErrorStatisticsChanged; 130 | 131 | /// 132 | /// Get player name in playing. 133 | /// 134 | public event OnPlayerChangedEvt OnPlayerChanged; 135 | 136 | /// 137 | /// Get play type and hit events in playing. (https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osr_(file_format)) 138 | /// 139 | public event OnHitEventsChangedEvt OnHitEventsChanged; 140 | ``` 141 | 142 | ##### OsuStatus ***enum*** 143 | ```csharp 144 | public enum OsuStatus 145 | { 146 | NoFoundProcess, 147 | Unkonwn, 148 | Listening, 149 | Playing, 150 | Editing, 151 | Rank 152 | } 153 | ``` 154 | # How to use?(Dev) 155 | ```csharp 156 | var ortdp = getHoster().EnumPluings().FirstOrDefault(p => p.Name == "OsuRTDataProvider") as OsuRTDataProviderPlugin; 157 | ortdp.ListenerManager.OnBeatmapChanged+=(b)=>{/*...*/}; 158 | ``` 159 | -------------------------------------------------------------------------------- /Setting.cs: -------------------------------------------------------------------------------- 1 | using Sync.Tools.ConfigurationAttribute; 2 | using Sync.Tools; 3 | using System; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace OsuRTDataProvider 8 | { 9 | public class SettingIni : IConfigurable 10 | { 11 | [Integer(MinValue = 1,MaxValue = 10000)] 12 | public ConfigurationElement ListenInterval 13 | { 14 | set => Setting.ListenInterval = int.Parse(value); 15 | get => Setting.ListenInterval.ToString(); 16 | } 17 | 18 | [Bool(RequireRestart = true)] 19 | public ConfigurationElement EnableTourneyMode 20 | { 21 | get => Setting.EnableTourneyMode.ToString(); 22 | set => Setting.EnableTourneyMode = bool.Parse(value); 23 | } 24 | 25 | [Integer(MinValue = 1,MaxValue = 8,RequireRestart = true)] 26 | public ConfigurationElement TeamSize { 27 | get => Setting.TeamSize.ToString(); 28 | set 29 | { 30 | Setting.TeamSize = int.Parse(value); 31 | if (Setting.TeamSize < 1 || Setting.TeamSize > 8) 32 | { 33 | Setting.TeamSize = 0; 34 | Logger.Info("TeamSize must be between 1 and 8."); 35 | } 36 | } 37 | } 38 | 39 | [Bool(RequireRestart = true)] 40 | public ConfigurationElement DebugMode 41 | { 42 | get => Setting.DebugMode.ToString(); 43 | set => Setting.DebugMode = bool.Parse(value); 44 | } 45 | 46 | [Path(IsDirectory = true, RequireRestart = true)] 47 | public ConfigurationElement ForceOsuSongsDirectory 48 | { 49 | get => Setting.ForceOsuSongsDirectory; 50 | set => Setting.ForceOsuSongsDirectory = value; 51 | } 52 | //Auto,Osu,Taiko,Mania,CTB 53 | [List(ValueList = new string[] { "Auto", "Osu", "Taiko", "CatchTheBeat", "Mania" })] 54 | public ConfigurationElement GameMode 55 | { 56 | get => Setting.GameMode; 57 | set => Setting.GameMode = value; 58 | } 59 | 60 | [Bool] 61 | public ConfigurationElement DisableProcessNotFoundInformation 62 | { 63 | set => Setting.DisableProcessNotFoundInformation = bool.Parse(value); 64 | get => Setting.DisableProcessNotFoundInformation.ToString(); 65 | } 66 | 67 | [Bool] 68 | public ConfigurationElement EnableModsChangedAtListening 69 | { 70 | set => Setting.EnableModsChangedAtListening = bool.Parse(value); 71 | get => Setting.EnableModsChangedAtListening.ToString(); 72 | } 73 | 74 | public void onConfigurationLoad() 75 | { 76 | } 77 | 78 | public void onConfigurationReload() 79 | { 80 | } 81 | 82 | public void onConfigurationSave() 83 | { 84 | } 85 | } 86 | 87 | internal static class Setting 88 | { 89 | public static bool DebugMode = false; 90 | public static int ListenInterval = 100;//ms 91 | public static bool EnableTourneyMode = false; 92 | public static int TeamSize = 1; 93 | public static string ForceOsuSongsDirectory = ""; 94 | public static string GameMode = "Auto"; 95 | public static bool DisableProcessNotFoundInformation = false; 96 | public static bool EnableModsChangedAtListening = false; 97 | 98 | #region NoSave 99 | public static string SongsPath = string.Empty; 100 | public static string OsuVersion = string.Empty; 101 | public static string Username = string.Empty; 102 | #endregion 103 | 104 | public static double CurrentOsuVersionValue => Utils.ConvertVersionStringToValue(OsuVersion); 105 | } 106 | } -------------------------------------------------------------------------------- /UpdateChecker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Runtime.InteropServices; 7 | using System.Text; 8 | using System.Text.RegularExpressions; 9 | using System.Threading.Tasks; 10 | 11 | namespace OsuRTDataProvider 12 | { 13 | static class UpdateChecker 14 | { 15 | private static readonly Regex NAME_REGEX = new Regex(@"""name"":\s*""v(\d+\.\d+\.\d+)"""); 16 | 17 | private const string LATEST_RELEASE_URL = "https://api.github.com/repos/OsuSync/OsuRTDataProvider/releases/latest"; 18 | 19 | public static void CheckUpdate() 20 | { 21 | try 22 | { 23 | string data = GetHttpData(LATEST_RELEASE_URL); 24 | var groups = NAME_REGEX.Match(data).Groups; 25 | string ortdp_version = groups[1].Value; 26 | bool has_update = CheckRtppUpdate(ortdp_version); 27 | 28 | if(has_update) 29 | { 30 | Logger.Warn(DefaultLanguage.CHECK_GOTO_RELEASE_PAGE_HINT); 31 | } 32 | } 33 | catch (Exception e) 34 | { 35 | Logger.Error(e.ToString()); 36 | } 37 | } 38 | 39 | private static bool CheckRtppUpdate(string tag) 40 | { 41 | Version ver = Version.Parse(tag); 42 | Version selfVer = Version.Parse(OsuRTDataProviderPlugin.VERSION); 43 | if (ver > selfVer) 44 | { 45 | Logger.Warn(string.Format(DefaultLanguage.LANG_CHECK_ORTDP_UPDATE, ver)); 46 | return true; 47 | } 48 | return false; 49 | } 50 | 51 | private static string GetHttpData(string url) 52 | { 53 | ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls | SecurityProtocolType.Ssl3; 54 | HttpWebRequest wReq = (HttpWebRequest)WebRequest.Create(url); 55 | wReq.UserAgent = "OsuSync"; 56 | WebResponse wResp = wReq.GetResponse(); 57 | Stream respStream = wResp.GetResponseStream(); 58 | 59 | using (StreamReader reader = new StreamReader(respStream, Encoding.UTF8)) 60 | { 61 | return reader.ReadToEnd(); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Utils.cs: -------------------------------------------------------------------------------- 1 | using Sync.Tools; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Text.RegularExpressions; 8 | using System.Threading.Tasks; 9 | 10 | namespace OsuRTDataProvider 11 | { 12 | public static class Utils 13 | { 14 | public static double ConvertVersionStringToValue(string osu_version_string) 15 | { 16 | if (double.TryParse(Regex.Match(osu_version_string, @"\d+(\.\d*)?").Value.ToString(),NumberStyles.Float, CultureInfo.InvariantCulture, out var ver)) 17 | { 18 | 19 | return ver; 20 | } 21 | 22 | throw new Exception("Can't parse version: "+osu_version_string); 23 | } 24 | 25 | //https://gist.github.com/peppy/3a11cb58c856b6af7c1916422f668899 26 | public static List GetErrorStatisticsArray(List list) 27 | { 28 | if (list == null || list.Count == 0) 29 | return null; 30 | List result = new List(4); 31 | double total = 0, _total = 0, totalAll = 0; 32 | int count = 0, _count = 0; 33 | int max = 0, min = int.MaxValue; 34 | for (int i = 0; i < list.Count; i++) 35 | { 36 | if (list[i] > max) 37 | max = list[i]; 38 | if (list[i] < min) 39 | min = list[i]; 40 | totalAll += list[i]; 41 | if (list[i] >= 0) 42 | { 43 | total += list[i]; 44 | count++; 45 | } 46 | else 47 | { 48 | _total += list[i]; 49 | _count++; 50 | } 51 | } 52 | double avarage = totalAll / list.Count; 53 | double variance = 0; 54 | for (int i = 0; i < list.Count; i++) 55 | { 56 | variance += Math.Pow(list[i] - avarage, 2); 57 | } 58 | variance = variance / list.Count; 59 | result.Add(_count == 0 ? 0 : _total / _count); //0 60 | result.Add(count == 0 ? 0 : total / count); //1 61 | result.Add(avarage); //2 62 | result.Add(variance); //3 63 | result.Add(Math.Sqrt(variance)); //4 64 | result.Add(max); //5 65 | result.Add(min); //6 66 | return result; 67 | } 68 | } 69 | 70 | public static class Logger 71 | { 72 | static Logger logger=new Logger(); 73 | 74 | public static void Info(string message) => logger.LogInfomation(message); 75 | 76 | public static void Debug(string message) 77 | { 78 | if (Setting.DebugMode) 79 | logger.LogInfomation(message); 80 | } 81 | 82 | public static void Error(string message) => logger.LogError(message); 83 | public static void Warn(string message) => logger.LogWarning(message); 84 | } 85 | } 86 | --------------------------------------------------------------------------------