├── osu!rx ├── Core │ ├── HitScanResult.cs │ ├── Timewarp.cs │ └── Relax.cs ├── Configuration │ ├── Playstyles.cs │ └── ConfigManager.cs ├── packages.config ├── osu │ ├── Memory │ │ ├── Objects │ │ │ ├── OsuObject.cs │ │ │ ├── OsuStates.cs │ │ │ ├── OsuHitObjectManager.cs │ │ │ ├── OsuPlayer.cs │ │ │ └── OsuConfigManager.cs │ │ ├── Signatures.cs │ │ └── OsuProcess.cs │ ├── OsuWindow.cs │ └── OsuManager.cs ├── App.config ├── Helpers │ ├── CryptoHelper.cs │ ├── Extensions.cs │ └── CurveHelper.cs ├── Dependencies │ └── DependencyContainer.cs ├── Properties │ └── AssemblyInfo.cs ├── osu!rx.csproj └── Program.cs ├── LICENSE ├── osu!rx.sln ├── README.md └── .gitignore /osu!rx/Core/HitScanResult.cs: -------------------------------------------------------------------------------- 1 | namespace osu_rx.Core 2 | { 3 | public enum HitScanResult 4 | { 5 | CanHit, 6 | ShouldHit, 7 | Wait, 8 | MoveToNextObject 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /osu!rx/Configuration/Playstyles.cs: -------------------------------------------------------------------------------- 1 | namespace osu_rx.Configuration 2 | { 3 | public enum PlayStyles 4 | { 5 | Singletap, 6 | Alternate, 7 | MouseOnly, 8 | TapX 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /osu!rx/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /osu!rx/osu/Memory/Objects/OsuObject.cs: -------------------------------------------------------------------------------- 1 | using osu_rx.Dependencies; 2 | using System; 3 | 4 | namespace osu_rx.osu.Memory.Objects 5 | { 6 | public abstract class OsuObject 7 | { 8 | protected OsuProcess OsuProcess; 9 | 10 | public virtual UIntPtr BaseAddress { get; protected set; } 11 | 12 | public OsuObject() => OsuProcess = DependencyContainer.Get(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /osu!rx/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /osu!rx/osu/Memory/Objects/OsuStates.cs: -------------------------------------------------------------------------------- 1 | namespace osu_rx.osu.Memory.Objects 2 | { 3 | public enum OsuStates 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 | } 31 | -------------------------------------------------------------------------------- /osu!rx/Helpers/CryptoHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Security.Cryptography; 3 | 4 | namespace osu_rx.Helpers 5 | { 6 | public class CryptoHelper 7 | { 8 | private static readonly NumberFormatInfo numberFormat = new CultureInfo(@"en-US", false).NumberFormat; 9 | private static MD5 md5 = MD5.Create(); 10 | 11 | public static string GetMD5String(byte[] data) 12 | { 13 | lock (md5) 14 | data = md5.ComputeHash(data); 15 | 16 | char[] str = new char[data.Length * 2]; 17 | for (int i = 0; i < data.Length; i++) 18 | data[i].ToString("x2", numberFormat).CopyTo(0, str, i * 2, 2); 19 | 20 | return new string(str); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /osu!rx/Helpers/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | using System.Numerics; 4 | 5 | namespace osu_rx.Helpers 6 | { 7 | public static class Extensions 8 | { 9 | public static float NextFloat(this Random random, float min, float max) => (float)random.NextDouble() * (max - min) + min; 10 | 11 | public static bool AlmostEquals(this double d, double value, double allowance) => Math.Abs(d - value) <= allowance; 12 | 13 | public static bool AlmostEquals(this float f, float value, float allowance) => Math.Abs(f - value) <= allowance; 14 | 15 | public static Vector2 ToVector2(this Point point) => new Vector2(point.X, point.Y); 16 | 17 | public static float Clamp(this float value, float min, float max) => value < min ? min : value > max ? max : value; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /osu!rx/Dependencies/DependencyContainer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace osu_rx.Dependencies 5 | { 6 | public class DependencyContainer 7 | { 8 | private static List dependencies = new List(); 9 | 10 | public static void Cache(T dependency) => dependencies.Add(dependency); 11 | 12 | public static T Get() 13 | { 14 | var dependencyType = typeof(T); 15 | 16 | var dependency = dependencies.Find(d => d.GetType() == dependencyType); 17 | if (dependency != default) 18 | return (T)dependency; 19 | else if (dependency == null) 20 | throw new Exception($"Dependency of type {dependencyType} not cached!"); 21 | else 22 | throw new Exception($"Dependency of type {dependencyType} not found!"); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 mrflashstudio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /osu!rx.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29519.87 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu!rx", "osu!rx\osu!rx.csproj", "{F4521520-4650-47DD-BE31-E44C21D0EEE6}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {F4521520-4650-47DD-BE31-E44C21D0EEE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {F4521520-4650-47DD-BE31-E44C21D0EEE6}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {F4521520-4650-47DD-BE31-E44C21D0EEE6}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {F4521520-4650-47DD-BE31-E44C21D0EEE6}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {51C22103-DE21-4AD0-9D58-663B5D2B95E8} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /osu!rx/osu/Memory/Signatures.cs: -------------------------------------------------------------------------------- 1 | namespace osu_rx.osu.Memory 2 | { 3 | public static class Signatures 4 | { 5 | public static readonly Signature Time = new Signature 6 | { 7 | Pattern = "D9 58 2C 8B 3D ?? ?? ?? ?? 8B 1D", 8 | Offset = 0xB 9 | }; 10 | 11 | public const int IsAudioPlayingOffset = 0x30; 12 | 13 | public static readonly Signature State = new Signature 14 | { 15 | Pattern = "8D 45 BC 89 46 0C 83 3D", 16 | Offset = 0x8 17 | }; 18 | 19 | public static readonly Signature ReplayMode = new Signature 20 | { 21 | Pattern = "85 C0 75 0D 80 3D", 22 | Offset = 0x6 23 | }; 24 | 25 | public static readonly Signature ConfigManager = new Signature 26 | { 27 | Pattern = "8B 45 DC 8B 0D", 28 | Offset = 0x5 29 | }; 30 | 31 | public static readonly Signature Player = new Signature 32 | { 33 | Pattern = "FF 50 0C 8B D8 8B 15", 34 | Offset = 0x7 35 | }; 36 | 37 | //TODO: i couldn't create signature for this one :( 38 | public static readonly int[] AudioRateOffsets = new int[] 39 | { 40 | 0x00034268, 41 | 0x8, 42 | 0x10, 43 | 0xC, 44 | 0x40 45 | }; 46 | } 47 | 48 | public class Signature 49 | { 50 | public string Pattern; 51 | public int Offset; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /osu!rx/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // Общие сведения об этой сборке предоставляются следующим набором 5 | // набора атрибутов. Измените значения этих атрибутов для изменения сведений, 6 | // связанные с этой сборкой. 7 | [assembly: AssemblyTitle("osu!rx")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("osu!rx")] 12 | [assembly: AssemblyCopyright("Copyright © 2020")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Установка значения False для параметра ComVisible делает типы в этой сборке невидимыми 17 | // для компонентов COM. Если необходимо обратиться к типу в этой сборке через 18 | // из модели COM задайте для атрибута ComVisible этого типа значение true. 19 | [assembly: ComVisible(false)] 20 | 21 | // Следующий GUID представляет идентификатор typelib, если этот проект доступен из модели COM 22 | [assembly: Guid("f4521520-4650-47dd-be31-e44c21d0eee6")] 23 | 24 | // Сведения о версии сборки состоят из указанных ниже четырех значений: 25 | // 26 | // Основной номер версии 27 | // Дополнительный номер версии 28 | // Номер сборки 29 | // Номер редакции 30 | // 31 | // Можно задать все значения или принять номера сборки и редакции по умолчанию 32 | // используя "*", как показано ниже: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.1.2.0")] 35 | [assembly: AssemblyFileVersion("1.1.2.0")] 36 | -------------------------------------------------------------------------------- /osu!rx/Core/Timewarp.cs: -------------------------------------------------------------------------------- 1 | using osu_rx.Dependencies; 2 | using osu_rx.osu; 3 | using osu_rx.osu.Memory; 4 | using System; 5 | using System.Diagnostics; 6 | 7 | namespace osu_rx.Core 8 | { 9 | public class Timewarp 10 | { 11 | private const double defaultRate = 1147; 12 | 13 | private OsuManager osuManager; 14 | private UIntPtr audioRateAddress = UIntPtr.Zero; 15 | 16 | public Timewarp() => osuManager = DependencyContainer.Get(); 17 | 18 | public void Refresh() 19 | { 20 | foreach (ProcessModule module in osuManager.OsuProcess.Process.Modules) 21 | { 22 | if (module.ModuleName == "bass.dll") 23 | { 24 | audioRateAddress = (UIntPtr)module.BaseAddress.ToInt32(); 25 | break; 26 | } 27 | } 28 | 29 | for (int i = 0; i < Signatures.AudioRateOffsets.Length; i++) 30 | { 31 | audioRateAddress += Signatures.AudioRateOffsets[i]; 32 | 33 | if (i != Signatures.AudioRateOffsets.Length - 1) 34 | audioRateAddress = (UIntPtr)osuManager.OsuProcess.ReadInt32(audioRateAddress); 35 | } 36 | } 37 | 38 | public void Update(double rate, double initialRate) 39 | { 40 | if (osuManager.OsuProcess.ReadDouble(audioRateAddress) != rate) 41 | { 42 | osuManager.OsuProcess.WriteMemory(audioRateAddress, BitConverter.GetBytes(rate), sizeof(double)); 43 | osuManager.OsuProcess.WriteMemory(audioRateAddress + 0x8, BitConverter.GetBytes(rate * defaultRate), sizeof(double)); 44 | } 45 | 46 | //bypassing audio checks 47 | osuManager.Player.AudioCheckTime = (int)(osuManager.CurrentTime * initialRate); 48 | } 49 | 50 | public void Reset() 51 | { 52 | osuManager.OsuProcess.WriteMemory(audioRateAddress, BitConverter.GetBytes(1), sizeof(double)); 53 | osuManager.OsuProcess.WriteMemory(audioRateAddress + 0x8, BitConverter.GetBytes(defaultRate), sizeof(double)); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /osu!rx/osu/OsuWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | using System.Numerics; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace osu_rx.osu 7 | { 8 | public class OsuWindow 9 | { 10 | [DllImport("user32.dll")] 11 | private static extern bool GetClientRect(IntPtr hwnd, out Rect rectangle); 12 | 13 | [DllImport("user32.dll")] 14 | private static extern bool ClientToScreen(IntPtr hwnd, out Point point); 15 | 16 | private struct Rect 17 | { 18 | public int Left { get; set; } 19 | public int Top { get; set; } 20 | public int Right { get; set; } 21 | public int Bottom { get; set; } 22 | } 23 | 24 | private IntPtr windowHandle; 25 | 26 | public Vector2 WindowSize 27 | { 28 | get 29 | { 30 | GetClientRect(windowHandle, out var osuRectangle); 31 | return new Vector2(osuRectangle.Right, osuRectangle.Bottom); 32 | } 33 | } 34 | 35 | public Vector2 WindowPosition 36 | { 37 | get 38 | { 39 | ClientToScreen(windowHandle, out var osuPosition); 40 | return new Vector2(osuPosition.X, osuPosition.Y); 41 | } 42 | } 43 | 44 | public float WindowRatio 45 | { 46 | get => WindowSize.Y / 480; 47 | } 48 | 49 | public Vector2 PlayfieldSize 50 | { 51 | get 52 | { 53 | float width = 512 * WindowRatio; 54 | float height = 384 * WindowRatio; 55 | return new Vector2(width, height); 56 | } 57 | } 58 | 59 | public Vector2 PlayfieldPosition //topleft origin 60 | { 61 | get 62 | { 63 | var windowCentre = WindowSize / 2; 64 | float x = windowCentre.X - PlayfieldSize.X / 2; 65 | float y = windowCentre.Y - PlayfieldSize.Y / 2; 66 | return new Vector2(x, y); 67 | } 68 | } 69 | 70 | public float PlayfieldRatio 71 | { 72 | get => PlayfieldSize.Y / 384; 73 | } 74 | 75 | public OsuWindow(IntPtr handle) => windowHandle = handle; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # osu-rx 2 | osu!standard relax hack 3 | 4 | ## Status 5 | osu!rx is still under development. Bug reports, feature requests, pull requests and any other help or feedback are very much appreciated! 6 | 7 | Hit Timing randomization and HitScan need improvements and your feedback. Take a look at the code to get started! 8 | - [Hit Timings randomization](https://github.com/mrflashstudio/osu-rx/blob/master/osu!rx/Core/Relax.cs#L254) 9 | - [HitScan](https://github.com/mrflashstudio/osu-rx/blob/master/osu!rx/Core/Relax.cs#L209) 10 | 11 | ## Features 12 | - **Automatic beatmap detection:** you don't need to alt-tab from osu! to select beatmap manually, osu!rx will do all the dirty work for you. 13 | 14 | - **Playstyles:** osu!rx has 4 most(?) popular built-in playstyles that you can select! 15 | - Singletap 16 | - Alternate 17 | - Mouse only 18 | - TapX 19 | 20 | - **Hit Timing randomization:** osu!rx automatically randomizes click timings depending on whether you're currently alternating or not. 21 | 22 | - **HitWindow100 Key:** if this key is pressed, osu!rx will widen current hit timings so you can get more 100s and slightly increase your Unstable Rate. 23 | 24 | - **HitScan:** it will scan for current cursor position and determine whether relax should hit right now, earlier or later. 25 | 26 | - **osu!rx won't ever be outdated:** osu!rx has IPC Fallback mode which will turn on if memory scanning fails. While this mode is on, osu!rx will communicate with osu! through IPC to get needed variables. So if there will be any breaking-updates to osu!, most of osu!rx's functions will continue to work. 27 | 28 | ## Running osu!rx 29 | **Download latest build from one of the sources below:** 30 | | [MPGH (every build is approved by forum admin)](https://www.mpgh.net/forum/showthread.php?t=1488076) | Github releases will be available soon! | 31 | |-----------------------------------------------|-----------| 32 | 33 | *Paranoids can compile source code themselves ;)* 34 | 35 | - Extract downloaded archive into any folder. 36 | - Launch osu!, then run osu!rx.exe 37 | - Change any setting you want. 38 | - Select "Start relax" option in the main menu. 39 | - Go back to osu! and select beatmap you want to play. 40 | - Start playing! 41 | 42 | ### Requirements 43 | - .net framework 4.7.2 is required to run osu!rx. You can download it [here](https://dotnet.microsoft.com/download/thank-you/net472). 44 | 45 | ### Important notes 46 | - If you plan on changing executable's name, then change the name of *"osu!rx.exe.config"* too or else it will crash. 47 | 48 | - If you see something like *"Unhandled exception: System.IO.FileNotFoundException: ..."* follow these steps: 49 | - Right click on downloaded archive. 50 | - Click "Properties". 51 | - Click "Unblock". 52 | - Click "Apply" and repeat all steps described in **Running osu!rx** section. 53 | ![s](https://i.ibb.co/jZY8fk0/image.png) 54 | 55 | ### Detection state 56 | At this moment osu!rx is undetected and its detection state is constantly tracked by me and other users. 57 | 58 | ### Demonstation video 59 | ***osu!rx does not affect performance. In this case lags were caused by obs and cute girls in the background.*** 60 | [![Video](https://i.ibb.co/grQSzMP/screenshot065.png)](https://www.youtube.com/watch?v=1FUxnGqjASQ) 61 | -------------------------------------------------------------------------------- /osu!rx/Configuration/ConfigManager.cs: -------------------------------------------------------------------------------- 1 | using SimpleIniConfig; 2 | using WindowsInput.Native; 3 | 4 | namespace osu_rx.Configuration 5 | { 6 | public class ConfigManager 7 | { 8 | private Config config; 9 | 10 | public PlayStyles PlayStyle 11 | { 12 | get => config.GetValue("PlayStyle", PlayStyles.Singletap); 13 | set => config.SetValue("PlayStyle", value); 14 | } 15 | 16 | public VirtualKeyCode PrimaryKey 17 | { 18 | get => config.GetValue("PrimaryKey", VirtualKeyCode.VK_Z); 19 | set => config.SetValue("PrimaryKey", value); 20 | } 21 | 22 | public VirtualKeyCode SecondaryKey 23 | { 24 | get => config.GetValue("SecondaryKey", VirtualKeyCode.VK_X); 25 | set => config.SetValue("SecondaryKey", value); 26 | } 27 | 28 | public VirtualKeyCode HitWindow100Key 29 | { 30 | get => config.GetValue("HitWindow100Key", VirtualKeyCode.SPACE); 31 | set => config.SetValue("HitWindow100Key", value); 32 | } 33 | 34 | public int MaxSingletapBPM 35 | { 36 | get => config.GetValue("MaxSingletapBPM", 250); 37 | set => config.SetValue("MaxSingletapBPM", value); 38 | } 39 | 40 | public int AudioOffset 41 | { 42 | get => config.GetValue("AudioOffset", 0); 43 | set => config.SetValue("AudioOffset", value); 44 | } 45 | 46 | public bool UseCustomWindowTitle 47 | { 48 | get => config.GetValue("UseCustomWindowTitle", false); 49 | set => config.SetValue("UseCustomWindowTitle", value); 50 | } 51 | 52 | public string CustomWindowTitle 53 | { 54 | get => config.GetValue("CustomWindowTitle", string.Empty); 55 | set => config.SetValue("CustomWindowTitle", value); 56 | } 57 | 58 | public bool EnableHitScan 59 | { 60 | get => config.GetValue("EnableHitScan", true); 61 | set => config.SetValue("EnableHitScan", value); 62 | } 63 | 64 | public int HoldBeforeSpinnerTime 65 | { 66 | get => config.GetValue("HoldBeforeSpinnerTime", 500); 67 | set => config.SetValue("HoldBeforeSpinnerTime", value); 68 | } 69 | 70 | public bool EnableHitScanPrediction 71 | { 72 | get => config.GetValue("HitscanEnablePrediction", true); 73 | set => config.SetValue("HitscanEnablePrediction", value); 74 | } 75 | 76 | public float HitScanRadiusMultiplier 77 | { 78 | get => config.GetValue("HitscanRadiusMultiplier", 0.9f); 79 | set => config.SetValue("HitscanRadiusMultiplier", value); 80 | } 81 | 82 | public int HitScanRadiusAdditional 83 | { 84 | get => config.GetValue("HitscanRadiusAdditional", 50); 85 | set => config.SetValue("HitscanRadiusAdditional", value); 86 | } 87 | 88 | public int HitScanMaxDistance 89 | { 90 | get => config.GetValue("HitscanMaxDistance", 30); 91 | set => config.SetValue("HitscanMaxDistance", value); 92 | } 93 | 94 | public int HitScanMissChance 95 | { 96 | get => config.GetValue("HitScanMissChance", 20); 97 | set => config.SetValue("HitScanMissChance", value); 98 | } 99 | 100 | public bool HitScanMissAfterHitWindow50 101 | { 102 | get => config.GetValue("HitScanMissAfterHitWindow50", true); 103 | set => config.SetValue("HitScanMissAfterHitWindow50", value); 104 | } 105 | 106 | public bool EnableTimewarp 107 | { 108 | get => config.GetValue("EnableTimewarp", false); 109 | set => config.SetValue("EnableTimewarp", value); 110 | } 111 | 112 | public double TimewarpRate 113 | { 114 | get => config.GetValue("TimewarpRate", 1); 115 | set => config.SetValue("TimewarpRate", value); 116 | } 117 | 118 | public ConfigManager() => config = new Config(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /osu!rx/osu!rx.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {F4521520-4650-47DD-BE31-E44C21D0EEE6} 8 | Exe 9 | osu_rx 10 | osu!rx 11 | v4.7.2 12 | 512 13 | true 14 | 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | ..\packages\OsuParsers.1.6.5\lib\netstandard2.0\OsuParsers.dll 38 | 39 | 40 | ..\packages\SimpleIniConfig.1.0.1\lib\netstandard2.0\SimpleIniConfig.dll 41 | 42 | 43 | 44 | 45 | 46 | 47 | ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ..\packages\InputSimulator.1.0.4.0\lib\net20\WindowsInput.dll 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /osu!rx/osu/Memory/Objects/OsuHitObjectManager.cs: -------------------------------------------------------------------------------- 1 | using OsuParsers.Beatmaps.Objects; 2 | using OsuParsers.Enums; 3 | using OsuParsers.Enums.Beatmaps; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Numerics; 7 | 8 | namespace osu_rx.osu.Memory.Objects 9 | { 10 | public class OsuHitObjectManager : OsuObject 11 | { 12 | public OsuHitObjectManager(UIntPtr pointerToBaseAddress) => BaseAddress = pointerToBaseAddress; 13 | 14 | public Mods CurrentMods 15 | { 16 | get 17 | { 18 | UIntPtr modsObjectPointer = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x34); 19 | int encryptedValue = OsuProcess.ReadInt32(modsObjectPointer + 0x08); 20 | int decryptionKey = OsuProcess.ReadInt32(modsObjectPointer + 0x0C); 21 | 22 | return (Mods)(encryptedValue ^ decryptionKey); 23 | } 24 | } 25 | 26 | public List HitObjects 27 | { 28 | get 29 | { 30 | List hitObjects = new List(); 31 | 32 | UIntPtr hitObjectsListPointer = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x48); 33 | UIntPtr hitObjectsList = (UIntPtr)OsuProcess.ReadInt32(hitObjectsListPointer + 0x4); 34 | int hitObjectsCount = OsuProcess.ReadInt32(hitObjectsListPointer + 0xC); 35 | 36 | for (int i = 0; i < hitObjectsCount; i++) 37 | { 38 | UIntPtr hitObjectPointer = (UIntPtr)OsuProcess.ReadInt32(hitObjectsList + 0x8 + 0x4 * i); 39 | 40 | HitObject hitObject = null; 41 | 42 | //TODO: expose this enum in osuparsers 43 | HitObjectType type = (HitObjectType)OsuProcess.ReadInt32(hitObjectPointer + 0x18); 44 | type &= ~HitObjectType.ComboOffset; 45 | type &= ~HitObjectType.NewCombo; 46 | 47 | int startTime = OsuProcess.ReadInt32(hitObjectPointer + 0x10); 48 | int endTime = OsuProcess.ReadInt32(hitObjectPointer + 0x14); 49 | HitSoundType hitSoundType = (HitSoundType)OsuProcess.ReadInt32(hitObjectPointer + 0x1C); 50 | Vector2 position = new Vector2(OsuProcess.ReadFloat(hitObjectPointer + 0x38), OsuProcess.ReadFloat(hitObjectPointer + 0x3C)); 51 | 52 | switch (type) 53 | { 54 | case HitObjectType.Circle: 55 | hitObject = new HitCircle(position, startTime, endTime, hitSoundType, null, false, 0); 56 | break; 57 | case HitObjectType.Slider: 58 | int repeats = OsuProcess.ReadInt32(hitObjectPointer + 0x20); 59 | double pixelLength = OsuProcess.ReadDouble(hitObjectPointer + 0x8); 60 | CurveType curveType = (CurveType)OsuProcess.ReadInt32(hitObjectPointer + 0xE8); 61 | UIntPtr sliderPointsPointer = (UIntPtr)OsuProcess.ReadInt32(hitObjectPointer + 0xC0); 62 | UIntPtr sliderPointsList = (UIntPtr)OsuProcess.ReadInt32(sliderPointsPointer + 0x4); 63 | int sliderPointsCount = OsuProcess.ReadInt32(sliderPointsPointer + 0xC); 64 | List sliderPoints = new List(); 65 | for (int j = 0; j < sliderPointsCount; j++) 66 | { 67 | UIntPtr sliderPoint = sliderPointsList + 0x8 + 0x8 * j; 68 | 69 | sliderPoints.Add(new Vector2(OsuProcess.ReadFloat(sliderPoint), OsuProcess.ReadFloat(sliderPoint + 0x4))); 70 | } 71 | hitObject = new Slider(position, startTime, endTime, hitSoundType, curveType, sliderPoints, repeats, pixelLength, false, 0); 72 | break; 73 | case HitObjectType.Spinner: 74 | hitObject = new Spinner(position, startTime, endTime, hitSoundType, null, false, 0); 75 | break; 76 | } 77 | 78 | hitObjects.Add(hitObject); 79 | } 80 | 81 | return hitObjects; 82 | } 83 | } 84 | } 85 | 86 | enum HitObjectType 87 | { 88 | Circle = 1 << 0, 89 | Slider = 1 << 1, 90 | NewCombo = 1 << 2, 91 | Spinner = 1 << 3, 92 | ComboOffset = 1 << 4 | 1 << 5 | 1 << 6, 93 | Hold = 1 << 7 94 | } 95 | } -------------------------------------------------------------------------------- /osu!rx/osu/Memory/Objects/OsuPlayer.cs: -------------------------------------------------------------------------------- 1 | using OsuParsers.Beatmaps; 2 | using OsuParsers.Enums; 3 | using System; 4 | using System.Linq; 5 | using System.Numerics; 6 | 7 | namespace osu_rx.osu.Memory.Objects 8 | { 9 | public class OsuPlayer : OsuObject 10 | { 11 | public UIntPtr PointerToBaseAddress { get; private set; } 12 | 13 | public override UIntPtr BaseAddress 14 | { 15 | get => (UIntPtr)OsuProcess.ReadInt32(PointerToBaseAddress); 16 | protected set { } 17 | } 18 | 19 | public OsuRuleset Ruleset 20 | { 21 | get => new OsuRuleset((UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x60)); 22 | } 23 | 24 | public OsuHitObjectManager HitObjectManager 25 | { 26 | get => new OsuHitObjectManager((UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x40)); 27 | } 28 | 29 | public Beatmap Beatmap 30 | { 31 | get 32 | { 33 | UIntPtr beatmapBase = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0xD4); 34 | var beatmap = new Beatmap(); 35 | 36 | int mode = OsuProcess.ReadInt32(beatmapBase + 0x114); 37 | beatmap.GeneralSection.Mode = (Ruleset)mode; 38 | beatmap.GeneralSection.ModeId = mode; 39 | beatmap.MetadataSection.Artist = OsuProcess.ReadString(beatmapBase + 0x18); 40 | beatmap.MetadataSection.Title = OsuProcess.ReadString(beatmapBase + 0x24); 41 | beatmap.MetadataSection.Creator = OsuProcess.ReadString(beatmapBase + 0x78); 42 | beatmap.MetadataSection.Version = OsuProcess.ReadString(beatmapBase + 0xA8); 43 | beatmap.DifficultySection.ApproachRate = OsuProcess.ReadFloat(beatmapBase + 0x2C); 44 | beatmap.DifficultySection.CircleSize = OsuProcess.ReadFloat(beatmapBase + 0x30); 45 | beatmap.DifficultySection.HPDrainRate = OsuProcess.ReadFloat(beatmapBase + 0x34); 46 | beatmap.DifficultySection.OverallDifficulty = OsuProcess.ReadFloat(beatmapBase + 0x38); 47 | beatmap.DifficultySection.SliderMultiplier = OsuProcess.ReadDouble(beatmapBase + 0x8); 48 | beatmap.DifficultySection.SliderTickRate = OsuProcess.ReadDouble(beatmapBase + 0x10); 49 | beatmap.HitObjects = HitObjectManager.HitObjects.ToList(); 50 | 51 | return beatmap; 52 | } 53 | } 54 | 55 | public int AudioCheckTime 56 | { 57 | get => OsuProcess.ReadInt32(BaseAddress + 0x154); 58 | set 59 | { 60 | OsuProcess.WriteMemory(BaseAddress + 0x154, BitConverter.GetBytes(value), sizeof(int)); 61 | OsuProcess.WriteMemory(BaseAddress + 0x158, BitConverter.GetBytes(value), sizeof(int)); 62 | } 63 | } 64 | 65 | public OsuPlayer(UIntPtr pointerToBaseAddress) => PointerToBaseAddress = pointerToBaseAddress; 66 | } 67 | 68 | public class OsuRuleset : OsuObject 69 | { 70 | public Vector2 MousePosition 71 | { 72 | get 73 | { 74 | //TODO: find a way to get position relative to playfield 75 | float x = OsuProcess.ReadFloat(BaseAddress + 0x7C); 76 | float y = OsuProcess.ReadFloat(BaseAddress + 0x80); 77 | 78 | return new Vector2(x, y); 79 | } 80 | } 81 | 82 | //ctb catcher position below 83 | /*public Vector2 MousePosition 84 | { 85 | get 86 | { 87 | IntPtr catcherAddress = (IntPtr)OsuProcess.ReadInt32(BaseAddress + 0x8C); 88 | float x = OsuProcess.ReadFloat(catcherAddress + 0x4C); 89 | float y = OsuProcess.ReadFloat(catcherAddress + 0x50); 90 | 91 | return new Vector2(x, y); 92 | } 93 | set 94 | { 95 | IntPtr wank = (IntPtr)OsuProcess.ReadInt32(BaseAddress + 0xA4); 96 | IntPtr vectorAddress = (IntPtr)OsuProcess.ReadInt32(BaseAddress + 0x8C); 97 | 98 | OsuProcess.WriteMemory(wank + 0x8, BitConverter.GetBytes(value.X), sizeof(float)); 99 | OsuProcess.WriteMemory(wank + 0xC, BitConverter.GetBytes(1), sizeof(int)); 100 | OsuProcess.WriteMemory(vectorAddress + 0x4C, BitConverter.GetBytes(value.X), sizeof(float)); 101 | OsuProcess.WriteMemory(vectorAddress + 0x50, BitConverter.GetBytes(value.Y), sizeof(float)); 102 | } 103 | }*/ 104 | 105 | public OsuRuleset(UIntPtr baseAddress) => BaseAddress = baseAddress; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /osu!rx/osu/Memory/Objects/OsuConfigManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace osu_rx.osu.Memory.Objects 5 | { 6 | public class OsuConfigManager : OsuObject 7 | { 8 | public string BeatmapDirectory 9 | { 10 | get 11 | { 12 | UIntPtr stringAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x27C); 13 | int stringLength = OsuProcess.ReadInt32(stringAddress + 0x4); 14 | 15 | return BitConverter.ToString(OsuProcess.ReadMemory(stringAddress + 0x8, (uint)stringLength)); 16 | } 17 | set 18 | { 19 | UIntPtr stringAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x27C); 20 | int stringLength = value.Length; 21 | 22 | //TODO: dunno if this is correct 23 | OsuProcess.WriteMemory(stringAddress + 0x4, BitConverter.GetBytes(stringLength), sizeof(int)); 24 | OsuProcess.WriteMemory(stringAddress + 0x8, Encoding.Default.GetBytes(value.ToCharArray(), 0, stringLength), (uint)stringLength); 25 | } 26 | } 27 | 28 | public bool Fullscreen 29 | { 30 | get 31 | { 32 | UIntPtr bindableAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x1C8); 33 | return OsuProcess.ReadBool(bindableAddress + 0xC); 34 | } 35 | set 36 | { 37 | UIntPtr bindableAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x1C8); 38 | OsuProcess.WriteMemory(bindableAddress + 0xC, BitConverter.GetBytes(value), sizeof(bool)); 39 | } 40 | } 41 | 42 | public bool Letterboxing 43 | { 44 | get 45 | { 46 | UIntPtr bindableAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x26C); 47 | return OsuProcess.ReadBool(bindableAddress + 0xC); 48 | } 49 | set 50 | { 51 | UIntPtr bindableAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x26C); 52 | OsuProcess.WriteMemory(bindableAddress + 0xC, BitConverter.GetBytes(value), sizeof(bool)); 53 | } 54 | } 55 | 56 | public int LetterboxingHorizontalPosition 57 | { 58 | get 59 | { 60 | UIntPtr bindableAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x270); 61 | return OsuProcess.ReadInt32(bindableAddress + 0x4); 62 | } 63 | set 64 | { 65 | UIntPtr bindableAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x270); 66 | OsuProcess.WriteMemory(bindableAddress + 0x4, BitConverter.GetBytes(value), sizeof(int)); 67 | } 68 | } 69 | 70 | public int LetterboxingVerticalPosition 71 | { 72 | get 73 | { 74 | UIntPtr bindableAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x274); 75 | return OsuProcess.ReadInt32(bindableAddress + 0x4); 76 | } 77 | set 78 | { 79 | UIntPtr bindableAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x274); 80 | OsuProcess.WriteMemory(bindableAddress + 0x4, BitConverter.GetBytes(value), sizeof(int)); 81 | } 82 | } 83 | 84 | public int Width 85 | { 86 | get 87 | { 88 | UIntPtr bindableAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x1A0); 89 | return OsuProcess.ReadInt32(bindableAddress + 0x4); 90 | } 91 | set 92 | { 93 | UIntPtr bindableAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x1A0); 94 | OsuProcess.WriteMemory(bindableAddress + 0x4, BitConverter.GetBytes(value), sizeof(int)); 95 | } 96 | } 97 | 98 | public int Height 99 | { 100 | get 101 | { 102 | UIntPtr bindableAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x17C); 103 | return OsuProcess.ReadInt32(bindableAddress + 0x4); 104 | } 105 | set 106 | { 107 | UIntPtr bindableAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x17C); 108 | OsuProcess.WriteMemory(bindableAddress + 0x4, BitConverter.GetBytes(value), sizeof(int)); 109 | } 110 | } 111 | 112 | public int WidthFullscreen 113 | { 114 | get 115 | { 116 | UIntPtr bindableAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x1A4); 117 | return OsuProcess.ReadInt32(bindableAddress + 0x4); 118 | } 119 | set 120 | { 121 | UIntPtr bindableAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x1A4); 122 | OsuProcess.WriteMemory(bindableAddress + 0x4, BitConverter.GetBytes(value), sizeof(int)); 123 | } 124 | } 125 | 126 | public int HeightFullscreen 127 | { 128 | get 129 | { 130 | UIntPtr bindableAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x180); 131 | return OsuProcess.ReadInt32(bindableAddress + 0x4); 132 | } 133 | set 134 | { 135 | UIntPtr bindableAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x180); 136 | OsuProcess.WriteMemory(bindableAddress + 0x4, BitConverter.GetBytes(value), sizeof(int)); 137 | } 138 | } 139 | 140 | public ScaleMode ScaleMode 141 | { 142 | get 143 | { 144 | UIntPtr bindableAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x1C0); 145 | return (ScaleMode)OsuProcess.ReadInt32(bindableAddress + 0xC); 146 | } 147 | set 148 | { 149 | UIntPtr bindableAddress = (UIntPtr)OsuProcess.ReadInt32(BaseAddress + 0x1C0); 150 | OsuProcess.WriteMemory(bindableAddress + 0xC, BitConverter.GetBytes((int)value), sizeof(int)); 151 | } 152 | } 153 | 154 | public OsuConfigManager(UIntPtr baseAddress) => BaseAddress = baseAddress; 155 | } 156 | 157 | public enum ScaleMode 158 | { 159 | Letterbox = 0, 160 | WidescreenConservative = 1, 161 | WidescreenAlways = 2 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | 352 | .idea/.idea.osu!rx/.idea/ 353 | 354 | .idea/.idea.osu!rx/riderModule.iml 355 | -------------------------------------------------------------------------------- /osu!rx/osu/Memory/OsuProcess.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | using static osu_rx.osu.Memory.OsuProcess; 7 | 8 | namespace osu_rx.osu.Memory 9 | { 10 | public class OsuProcess 11 | { 12 | [StructLayout(LayoutKind.Sequential)] 13 | public struct MEMORY_BASIC_INFORMATION 14 | { 15 | public UIntPtr BaseAddress; 16 | public UIntPtr AllocationBase; 17 | public uint AllocationProtect; 18 | public UIntPtr RegionSize; 19 | public MemoryState State; 20 | public MemoryProtect Protect; 21 | public MemoryType Type; 22 | } 23 | 24 | [DllImport("kernel32.dll", SetLastError = true)] 25 | private static extern bool ReadProcessMemory(IntPtr hProcess, UIntPtr lpBaseAddress, [Out] byte[] lpBuffer, uint dwSize, out UIntPtr lpNumberOfBytesRead); 26 | 27 | [DllImport("kernel32.dll", SetLastError = true)] 28 | public static extern bool WriteProcessMemory(IntPtr hProcess, UIntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out UIntPtr lpNumberOfBytesWritten); 29 | 30 | //TODO: x64 support 31 | [DllImport("kernel32.dll")] 32 | public static extern int VirtualQueryEx(IntPtr hProcess, UIntPtr lpAddress, out MEMORY_BASIC_INFORMATION lpBuffer, uint dwLength); 33 | 34 | public Process Process { get; private set; } 35 | 36 | public OsuProcess(Process process) => Process = process; 37 | 38 | public bool FindPattern(string pattern, out UIntPtr result) 39 | { 40 | byte?[] patternBytes = parsePattern(pattern); 41 | 42 | var regions = EnumerateMemoryRegions(); 43 | foreach (var region in regions) 44 | { 45 | if ((uint)region.BaseAddress < (uint)Process.MainModule.BaseAddress) 46 | continue; 47 | 48 | byte[] buffer = ReadMemory(region.BaseAddress, region.RegionSize.ToUInt32()); 49 | if (findMatch(patternBytes, buffer) is var match && match != UIntPtr.Zero) 50 | { 51 | result = (UIntPtr)(region.BaseAddress.ToUInt32() + match.ToUInt32()); 52 | return true; 53 | } 54 | } 55 | 56 | result = UIntPtr.Zero; 57 | return false; 58 | } 59 | 60 | public List EnumerateMemoryRegions() 61 | { 62 | var regions = new List(); 63 | UIntPtr address = UIntPtr.Zero; 64 | 65 | while (VirtualQueryEx(Process.Handle, address, out MEMORY_BASIC_INFORMATION basicInformation, (uint)Marshal.SizeOf(typeof(MEMORY_BASIC_INFORMATION))) != 0) 66 | { 67 | if (basicInformation.State != MemoryState.MemFree && !basicInformation.Protect.HasFlag(MemoryProtect.PageGuard)) 68 | regions.Add(new MemoryRegion(basicInformation)); 69 | 70 | address = (UIntPtr)(basicInformation.BaseAddress.ToUInt32() + basicInformation.RegionSize.ToUInt32()); 71 | } 72 | 73 | return regions; 74 | } 75 | 76 | public byte[] ReadMemory(UIntPtr address, uint size) 77 | { 78 | byte[] result = new byte[size]; 79 | ReadProcessMemory(Process.Handle, address, result, size, out UIntPtr bytesRead); 80 | return result; 81 | } 82 | 83 | public UIntPtr ReadMemory(UIntPtr address, byte[] buffer, uint size) 84 | { 85 | UIntPtr bytesRead; 86 | ReadProcessMemory(Process.Handle, address, buffer, size, out bytesRead); 87 | return bytesRead; 88 | } 89 | 90 | public void WriteMemory(UIntPtr address, byte[] data, uint length) 91 | { 92 | WriteProcessMemory(Process.Handle, address, data, length, out UIntPtr bytesWritten); 93 | } 94 | 95 | public int ReadInt32(UIntPtr address) => BitConverter.ToInt32(ReadMemory(address, sizeof(int)), 0); 96 | 97 | public uint ReadUInt32(UIntPtr address) => BitConverter.ToUInt32(ReadMemory(address, sizeof(uint)), 0); 98 | 99 | public long ReadInt64(UIntPtr address) => BitConverter.ToInt64(ReadMemory(address, sizeof(long)), 0); 100 | 101 | public ulong ReadUInt64(UIntPtr address) => BitConverter.ToUInt64(ReadMemory(address, sizeof(ulong)), 0); 102 | 103 | public float ReadFloat(UIntPtr address) => BitConverter.ToSingle(ReadMemory(address, sizeof(float)), 0); 104 | 105 | public double ReadDouble(UIntPtr address) => BitConverter.ToDouble(ReadMemory(address, sizeof(double)), 0); 106 | 107 | public bool ReadBool(UIntPtr address) => BitConverter.ToBoolean(ReadMemory(address, sizeof(bool)), 0); 108 | 109 | public string ReadString(UIntPtr address, Encoding encoding = null) 110 | { 111 | encoding = encoding ?? Encoding.UTF8; 112 | UIntPtr stringAddress = (UIntPtr)ReadInt32(address); 113 | int length = ReadInt32(stringAddress + 0x4) * (encoding == Encoding.UTF8 ? 2 : 1); 114 | 115 | return encoding.GetString(ReadMemory(stringAddress + 0x8, (uint)length)).Replace("\0", string.Empty); 116 | } 117 | 118 | private byte?[] parsePattern(string pattern) 119 | { 120 | byte?[] patternBytes = new byte?[pattern.Split(' ').Length]; 121 | for (int i = 0; i < patternBytes.Length; i++) 122 | { 123 | string currentByte = pattern.Split(' ')[i]; 124 | if (currentByte != "??") 125 | patternBytes[i] = Convert.ToByte(currentByte, 16); 126 | else 127 | patternBytes[i] = null; 128 | } 129 | 130 | return patternBytes; 131 | } 132 | 133 | private UIntPtr findMatch(byte?[] pattern, byte[] buffer) 134 | { 135 | bool found; 136 | for (int i = 0; i + pattern.Length <= buffer.Length; i++) 137 | { 138 | found = true; 139 | for (int j = 0; j < pattern.Length; j++) 140 | { 141 | if (pattern[j] == null || pattern[j] == buffer[i + j]) 142 | continue; 143 | 144 | found = false; 145 | break; 146 | } 147 | 148 | if (found) 149 | return (UIntPtr)i; 150 | } 151 | 152 | return UIntPtr.Zero; 153 | } 154 | } 155 | 156 | public class MemoryRegion 157 | { 158 | public UIntPtr BaseAddress { get; private set; } 159 | public UIntPtr RegionSize { get; private set; } 160 | public UIntPtr Start { get; private set; } 161 | public UIntPtr End { get; private set; } 162 | public MemoryState State { get; private set; } 163 | public MemoryProtect Protect { get; private set; } 164 | public MemoryType Type { get; private set; } 165 | 166 | public MemoryRegion(MEMORY_BASIC_INFORMATION basicInformation) 167 | { 168 | BaseAddress = basicInformation.BaseAddress; 169 | RegionSize = basicInformation.RegionSize; 170 | State = basicInformation.State; 171 | Protect = basicInformation.Protect; 172 | Type = basicInformation.Type; 173 | } 174 | } 175 | 176 | public enum MemoryState 177 | { 178 | MemCommit = 0x1000, 179 | MemReserved = 0x2000, 180 | MemFree = 0x10000 181 | } 182 | 183 | public enum MemoryType 184 | { 185 | MemPrivate = 0x20000, 186 | MemMapped = 0x40000, 187 | MemImage = 0x1000000 188 | } 189 | 190 | public enum MemoryProtect 191 | { 192 | PageNoAccess = 0x00000001, 193 | PageReadonly = 0x00000002, 194 | PageReadWrite = 0x00000004, 195 | PageWriteCopy = 0x00000008, 196 | PageExecute = 0x00000010, 197 | PageExecuteRead = 0x00000020, 198 | PageExecuteReadWrite = 0x00000040, 199 | PageExecuteWriteCopy = 0x00000080, 200 | PageGuard = 0x00000100, 201 | PageNoCache = 0x00000200, 202 | PageWriteCombine = 0x00000400 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /osu!rx/osu/OsuManager.cs: -------------------------------------------------------------------------------- 1 | using osu_rx.Dependencies; 2 | using osu_rx.Helpers; 3 | using osu_rx.osu.Memory; 4 | using osu_rx.osu.Memory.Objects; 5 | using OsuParsers.Enums; 6 | using System; 7 | using System.Diagnostics; 8 | using System.Drawing; 9 | using System.Linq; 10 | using System.Numerics; 11 | using System.Reflection; 12 | using System.Runtime.InteropServices; 13 | using System.Threading; 14 | 15 | namespace osu_rx.osu 16 | { 17 | public class OsuManager 18 | { 19 | [DllImport("user32.dll")] 20 | private static extern bool GetCursorPos(out Point point); 21 | 22 | private object interProcessOsu; 23 | private MethodInfo bulkClientDataMethod; 24 | 25 | public OsuProcess OsuProcess { get; private set; } 26 | 27 | public OsuWindow OsuWindow { get; private set; } 28 | 29 | public bool UsingIPCFallback { get; set; } 30 | 31 | public int CurrentTime 32 | { 33 | get 34 | { 35 | if (!UsingIPCFallback) 36 | return OsuProcess.ReadInt32(timeAddress); 37 | 38 | var data = bulkClientDataMethod.Invoke(interProcessOsu, null); 39 | return (int)data.GetType().GetField("MenuTime").GetValue(data); 40 | } 41 | } 42 | 43 | // Keep this function here to keep the program IPC-Safe 44 | public Mods CurrentMods 45 | { 46 | get 47 | { 48 | if (!UsingIPCFallback) 49 | return Player.HitObjectManager.CurrentMods; 50 | 51 | return Mods.None; 52 | } 53 | } 54 | 55 | public bool IsPaused 56 | { 57 | get 58 | { 59 | if (!UsingIPCFallback) 60 | return !OsuProcess.ReadBool(timeAddress + Signatures.IsAudioPlayingOffset); 61 | 62 | var data = bulkClientDataMethod.Invoke(interProcessOsu, null); 63 | return !(bool)data.GetType().GetField("AudioPlaying").GetValue(data); 64 | } 65 | } 66 | 67 | public bool IsPlayerLoaded 68 | { 69 | get 70 | { 71 | OsuProcess.Process.Refresh(); 72 | return OsuProcess.Process.MainWindowTitle.Contains('-'); 73 | } 74 | } 75 | 76 | public bool IsInReplayMode 77 | { 78 | get 79 | { 80 | if (!UsingIPCFallback) 81 | return OsuProcess.ReadBool(replayModeAddress); 82 | 83 | var data = bulkClientDataMethod.Invoke(interProcessOsu, null); 84 | return (bool)data.GetType().GetField("LReplayMode").GetValue(data); 85 | } 86 | } 87 | 88 | public string BeatmapChecksum 89 | { 90 | get 91 | { 92 | var data = bulkClientDataMethod.Invoke(interProcessOsu, null); 93 | return (string)data.GetType().GetField("BeatmapChecksum").GetValue(data); 94 | } 95 | } 96 | 97 | public OsuStates CurrentState 98 | { 99 | get 100 | { 101 | if (!UsingIPCFallback) 102 | return (OsuStates)OsuProcess.ReadInt32(stateAddress); 103 | 104 | var data = bulkClientDataMethod.Invoke(interProcessOsu, null); 105 | return (OsuStates)data.GetType().GetField("Mode").GetValue(data); 106 | } 107 | } 108 | 109 | public bool CanLoad 110 | { 111 | get => CurrentState == OsuStates.Play && !IsInReplayMode; 112 | } 113 | 114 | public bool CanPlay 115 | { 116 | get => CurrentState == OsuStates.Play && IsPlayerLoaded && !IsInReplayMode; 117 | } 118 | 119 | public Vector2 CursorPosition //relative to playfield 120 | { 121 | get 122 | { 123 | if (!UsingIPCFallback) 124 | return Player.Ruleset.MousePosition - OsuWindow.PlayfieldPosition; 125 | 126 | GetCursorPos(out var pos); 127 | return pos.ToVector2() - (OsuWindow.WindowPosition + OsuWindow.PlayfieldPosition); 128 | } 129 | } 130 | 131 | public float HitObjectScalingFactor(float circleSize) 132 | { 133 | return 1f - 0.7f * (float)AdjustDifficulty(circleSize); 134 | } 135 | 136 | public float HitObjectRadius(float circleSize) 137 | { 138 | float size = (float)(OsuWindow.PlayfieldSize.X / 8f * HitObjectScalingFactor(circleSize)); 139 | float radius = size / 2f / OsuWindow.PlayfieldRatio * 1.00041f; 140 | 141 | return radius; 142 | } 143 | 144 | public OsuPlayer Player { get; private set; } 145 | 146 | public string PathToOsu { get; private set; } 147 | 148 | public string SongsPath { get; private set; } 149 | 150 | public int HitWindow300(double od) => (int)DifficultyRange(od, 80, 50, 20); 151 | public int HitWindow100(double od) => (int)DifficultyRange(od, 140, 100, 60); 152 | public int HitWindow50(double od) => (int)DifficultyRange(od, 200, 150, 100); 153 | 154 | public double AdjustDifficulty(double difficulty) => (ApplyModsToDifficulty(difficulty, 1.3) - 5) / 5; 155 | 156 | public double ApplyModsToDifficulty(double difficulty, double hardrockFactor) 157 | { 158 | if (CurrentMods.HasFlag(Mods.Easy)) 159 | difficulty = Math.Max(0, difficulty / 2); 160 | if (CurrentMods.HasFlag(Mods.HardRock)) 161 | difficulty = Math.Min(10, difficulty * hardrockFactor); 162 | 163 | return difficulty; 164 | } 165 | 166 | public double DifficultyRange(double difficulty, double min, double mid, double max) 167 | { 168 | difficulty = ApplyModsToDifficulty(difficulty, 1.4); 169 | 170 | if (difficulty > 5) 171 | return mid + (max - mid) * (difficulty - 5) / 5; 172 | if (difficulty < 5) 173 | return mid - (mid - min) * (5 - difficulty) / 5; 174 | return mid; 175 | } 176 | 177 | public bool Initialize() 178 | { 179 | Console.WriteLine("Initializing..."); 180 | 181 | var osuProcess = Process.GetProcessesByName("osu!").FirstOrDefault(); 182 | 183 | if (osuProcess == default) 184 | { 185 | Console.WriteLine("\nosu! process not found! Please launch osu! first!"); 186 | return false; 187 | } 188 | 189 | osuProcess.EnableRaisingEvents = true; 190 | osuProcess.Exited += (o, e) => Environment.Exit(0); 191 | OsuProcess = new OsuProcess(osuProcess); 192 | DependencyContainer.Cache(OsuProcess); 193 | 194 | OsuWindow = new OsuWindow(osuProcess.MainWindowHandle); 195 | 196 | scanMemory(); 197 | connectToIPC(); 198 | 199 | return true; 200 | } 201 | 202 | private UIntPtr timeAddress; 203 | private UIntPtr stateAddress; 204 | private UIntPtr replayModeAddress; 205 | private void scanMemory() 206 | { 207 | try 208 | { 209 | Console.WriteLine("\nScanning for memory addresses (this may take a while)..."); 210 | 211 | //TODO: gooood this is dirty af 212 | if (OsuProcess.FindPattern(Signatures.Time.Pattern, out UIntPtr timeResult) 213 | && OsuProcess.FindPattern(Signatures.State.Pattern, out UIntPtr stateResult) 214 | && OsuProcess.FindPattern(Signatures.ReplayMode.Pattern, out UIntPtr replayModeResult) 215 | && OsuProcess.FindPattern(Signatures.Player.Pattern, out UIntPtr playerResult)) 216 | { 217 | timeAddress = (UIntPtr)OsuProcess.ReadInt32(timeResult + Signatures.Time.Offset); 218 | stateAddress = (UIntPtr)OsuProcess.ReadInt32(stateResult + Signatures.State.Offset); 219 | replayModeAddress = (UIntPtr)OsuProcess.ReadInt32(replayModeResult + Signatures.ReplayMode.Offset); 220 | Player = new OsuPlayer((UIntPtr)OsuProcess.ReadInt32(playerResult + Signatures.Player.Offset)); 221 | } 222 | } 223 | catch { } 224 | finally 225 | { 226 | if (timeAddress == UIntPtr.Zero || stateAddress == UIntPtr.Zero || replayModeAddress == UIntPtr.Zero 227 | || Player == null || Player.PointerToBaseAddress == UIntPtr.Zero) 228 | { 229 | Console.WriteLine("\nScanning failed! Using IPC fallback..."); 230 | UsingIPCFallback = true; 231 | Thread.Sleep(3000); 232 | } 233 | } 234 | } 235 | 236 | private void connectToIPC() 237 | { 238 | Console.WriteLine("\nConnecting to IPC..."); 239 | 240 | string assemblyPath = OsuProcess.Process.MainModule.FileName; 241 | 242 | var assembly = Assembly.LoadFrom(assemblyPath); 243 | var interProcessOsuType = assembly.ExportedTypes.First(a => a.FullName == "osu.Helpers.InterProcessOsu"); 244 | 245 | AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) => eventArgs.Name.Contains("osu!") ? Assembly.LoadFrom(assemblyPath) : null; 246 | 247 | interProcessOsu = Activator.GetObject(interProcessOsuType, "ipc://osu!/loader"); 248 | bulkClientDataMethod = interProcessOsuType.GetMethod("GetBulkClientData"); 249 | } 250 | } 251 | } -------------------------------------------------------------------------------- /osu!rx/Helpers/CurveHelper.cs: -------------------------------------------------------------------------------- 1 | using OsuParsers.Beatmaps.Objects; 2 | using OsuParsers.Enums.Beatmaps; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Numerics; 7 | 8 | namespace osu_rx.Helpers 9 | { 10 | //https://github.com/ppy/osu-framework/blob/master/osu.Framework/MathUtils/PathApproximator.cs 11 | public class CurveHelper 12 | { 13 | public static List ApproximateCircularArc(List points) 14 | { 15 | Vector2 a = points[0]; 16 | Vector2 b = points[1]; 17 | Vector2 c = points[2]; 18 | 19 | float aSq = (b - c).LengthSquared(); 20 | float bSq = (a - c).LengthSquared(); 21 | float cSq = (a - b).LengthSquared(); 22 | 23 | if (aSq.AlmostEquals(0, 1e-3f) || bSq.AlmostEquals(0, 1e-3f) || cSq.AlmostEquals(0, 1e-3f)) 24 | return new List(); 25 | 26 | float s = aSq * (bSq + cSq - aSq); 27 | float t = bSq * (aSq + cSq - bSq); 28 | float u = cSq * (aSq + bSq - cSq); 29 | 30 | float sum = s + t + u; 31 | 32 | if (sum.AlmostEquals(0, 1e-3f)) 33 | return new List(); 34 | 35 | Vector2 centre = (s * a + t * b + u * c) / sum; 36 | Vector2 dA = a - centre; 37 | Vector2 dC = c - centre; 38 | 39 | float r = dA.Length(); 40 | 41 | double thetaStart = Math.Atan2(dA.Y, dA.X); 42 | double thetaEnd = Math.Atan2(dC.Y, dC.X); 43 | 44 | while (thetaEnd < thetaStart) 45 | thetaEnd += 2 * Math.PI; 46 | 47 | double dir = 1; 48 | double thetaRange = thetaEnd - thetaStart; 49 | 50 | Vector2 orthoAtoC = c - a; 51 | orthoAtoC = new Vector2(orthoAtoC.Y, -orthoAtoC.X); 52 | 53 | if (Vector2.Dot(orthoAtoC, b - a) < 0) 54 | { 55 | dir = -dir; 56 | thetaRange = 2 * Math.PI - thetaRange; 57 | } 58 | 59 | int amountPoints = 2 * r <= 0.1f ? 2 : Math.Max(2, (int)Math.Ceiling(thetaRange / (2 * Math.Acos(1 - 0.1f / r)))); 60 | 61 | List output = new List(amountPoints); 62 | 63 | for (int i = 0; i < amountPoints; ++i) 64 | { 65 | double fract = (double)i / (amountPoints - 1); 66 | double theta = thetaStart + dir * fract * thetaRange; 67 | Vector2 o = new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)) * r; 68 | output.Add(centre + o); 69 | } 70 | 71 | return output; 72 | } 73 | 74 | public static List ApproximateCatmull(List points) 75 | { 76 | var result = new List(); 77 | 78 | for (int i = 0; i < points.Count - 1; i++) 79 | { 80 | var v1 = i > 0 ? points[i - 1] : points[i]; 81 | var v2 = points[i]; 82 | var v3 = i < points.Count - 1 ? points[i + 1] : v2 + v2 - v1; 83 | var v4 = i < points.Count - 2 ? points[i + 2] : v3 + v3 - v2; 84 | 85 | for (int c = 0; c < 50; c++) 86 | { 87 | result.Add(catmullFindPoint(ref v1, ref v2, ref v3, ref v4, (float)c / 50)); 88 | result.Add(catmullFindPoint(ref v1, ref v2, ref v3, ref v4, (float)(c + 1) / 50)); 89 | } 90 | } 91 | 92 | return result; 93 | } 94 | 95 | public static List ApproximateBezier(List points) 96 | { 97 | List output = new List(); 98 | 99 | if (points.Count == 0) 100 | return output; 101 | 102 | var subdivisionBuffer1 = new Vector2[points.Count]; 103 | var subdivisionBuffer2 = new Vector2[points.Count * 2 - 1]; 104 | 105 | Stack toFlatten = new Stack(); 106 | Stack freeBuffers = new Stack(); 107 | 108 | toFlatten.Push(points.ToArray()); 109 | 110 | Vector2[] leftChild = subdivisionBuffer2; 111 | 112 | while (toFlatten.Count > 0) 113 | { 114 | Vector2[] parent = toFlatten.Pop(); 115 | 116 | if (bezierIsFlatEnough(parent)) 117 | { 118 | bezierApproximate(parent, output, subdivisionBuffer1, subdivisionBuffer2, points.Count); 119 | 120 | freeBuffers.Push(parent); 121 | continue; 122 | } 123 | 124 | Vector2[] rightChild = freeBuffers.Count > 0 ? freeBuffers.Pop() : new Vector2[points.Count]; 125 | bezierSubdivide(parent, leftChild, rightChild, subdivisionBuffer1, points.Count); 126 | 127 | for (int i = 0; i < points.Count; ++i) 128 | parent[i] = leftChild[i]; 129 | 130 | toFlatten.Push(rightChild); 131 | toFlatten.Push(parent); 132 | } 133 | 134 | output.Add(points[points.Count - 1]); 135 | return output; 136 | } 137 | 138 | private static Vector2 catmullFindPoint(ref Vector2 vec1, ref Vector2 vec2, ref Vector2 vec3, ref Vector2 vec4, float t) 139 | { 140 | float t2 = t * t; 141 | float t3 = t * t2; 142 | 143 | Vector2 result; 144 | result.X = 0.5f * (2f * vec2.X + (-vec1.X + vec3.X) * t + (2f * vec1.X - 5f * vec2.X + 4f * vec3.X - vec4.X) * t2 + (-vec1.X + 3f * vec2.X - 3f * vec3.X + vec4.X) * t3); 145 | result.Y = 0.5f * (2f * vec2.Y + (-vec1.Y + vec3.Y) * t + (2f * vec1.Y - 5f * vec2.Y + 4f * vec3.Y - vec4.Y) * t2 + (-vec1.Y + 3f * vec2.Y - 3f * vec3.Y + vec4.Y) * t3); 146 | 147 | return result; 148 | } 149 | 150 | private static void bezierApproximate(Vector2[] points, List output, Vector2[] subdivisionBuffer1, Vector2[] subdivisionBuffer2, int count) 151 | { 152 | Vector2[] l = subdivisionBuffer2; 153 | Vector2[] r = subdivisionBuffer1; 154 | 155 | bezierSubdivide(points, l, r, subdivisionBuffer1, count); 156 | 157 | for (int i = 0; i < count - 1; ++i) 158 | l[count + i] = r[i + 1]; 159 | 160 | output.Add(points[0]); 161 | 162 | for (int i = 1; i < count - 1; ++i) 163 | { 164 | int index = 2 * i; 165 | Vector2 p = 0.25f * (l[index - 1] + 2 * l[index] + l[index + 1]); 166 | output.Add(p); 167 | } 168 | } 169 | 170 | private static void bezierSubdivide(Vector2[] points, Vector2[] l, Vector2[] r, Vector2[] subdivisionBuffer, int count) 171 | { 172 | Vector2[] midpoints = subdivisionBuffer; 173 | 174 | for (int i = 0; i < count; ++i) 175 | midpoints[i] = points[i]; 176 | 177 | for (int i = 0; i < count; i++) 178 | { 179 | l[i] = midpoints[0]; 180 | r[count - i - 1] = midpoints[count - i - 1]; 181 | 182 | for (int j = 0; j < count - i - 1; j++) 183 | midpoints[j] = (midpoints[j] + midpoints[j + 1]) / 2; 184 | } 185 | } 186 | 187 | private static bool bezierIsFlatEnough(Vector2[] points) 188 | { 189 | for (int i = 1; i < points.Length - 1; i++) 190 | { 191 | if ((points[i - 1] - 2 * points[i] + points[i + 1]).LengthSquared() > 0.25f * 0.25f * 4) 192 | return false; 193 | } 194 | 195 | return true; 196 | } 197 | } 198 | 199 | //https://github.com/ppy/osu/blob/master/osu.Game/Rulesets/Objects/SliderPath.cs 200 | public class SliderPath 201 | { 202 | public readonly double PixelLength; 203 | 204 | public readonly CurveType CurveType; 205 | 206 | public double Distance 207 | { 208 | get => cumulativeLength.Count == 0 ? 0 : cumulativeLength[cumulativeLength.Count - 1]; 209 | } 210 | 211 | private Slider slider; 212 | 213 | private Vector2[] sliderPoints; 214 | 215 | private List calculatedPath = new List(); 216 | private List cumulativeLength = new List(); 217 | 218 | public SliderPath(Slider slider) 219 | { 220 | this.slider = slider; 221 | 222 | sliderPoints = slider.SliderPoints.ToArray(); 223 | CurveType = slider.CurveType; 224 | PixelLength = slider.PixelLength; 225 | 226 | calculatePath(); 227 | calculateCumulativeLength(); 228 | } 229 | 230 | public double ProgressAt(double progress) 231 | { 232 | double p = progress * slider.Repeats % 1; 233 | if (progress * slider.Repeats % 2 == 1) 234 | p = 1 - p; 235 | 236 | return p; 237 | } 238 | 239 | public Vector2 PositionAt(double progress) 240 | { 241 | double d = progressToDistance(progress); 242 | return interpolateVertices(indexOfDistance(d), d); 243 | } 244 | 245 | private List calculateSubpath(List points) 246 | { 247 | switch (CurveType) 248 | { 249 | case CurveType.Linear: 250 | return points; 251 | 252 | case CurveType.PerfectCurve: 253 | if (sliderPoints.Length != 3 || points.ToArray().Length != 3) 254 | break; 255 | 256 | List subpath = CurveHelper.ApproximateCircularArc(points); 257 | 258 | if (subpath.Count == 0) 259 | break; 260 | 261 | return subpath; 262 | 263 | case CurveType.Catmull: 264 | return CurveHelper.ApproximateCatmull(points); 265 | } 266 | 267 | return CurveHelper.ApproximateBezier(points); 268 | } 269 | 270 | private void calculatePath() 271 | { 272 | int start = 0; 273 | int end = 0; 274 | 275 | for (int i = 0; i < sliderPoints.Length; ++i) 276 | { 277 | end++; 278 | 279 | if (i == sliderPoints.Length - 1 || sliderPoints[i] == sliderPoints[i + 1]) 280 | { 281 | var points = sliderPoints.Skip(start).Take(end - start).ToList(); 282 | 283 | foreach (Vector2 t in calculateSubpath(points)) 284 | { 285 | if (calculatedPath.Count == 0 || calculatedPath.Last() != t) 286 | calculatedPath.Add(t); 287 | } 288 | 289 | start = end; 290 | } 291 | } 292 | } 293 | 294 | private void calculateCumulativeLength() 295 | { 296 | double l = 0; 297 | 298 | cumulativeLength.Clear(); 299 | cumulativeLength.Add(l); 300 | 301 | for (int i = 0; i < calculatedPath.Count - 1; ++i) 302 | { 303 | Vector2 diff = calculatedPath[i + 1] - calculatedPath[i]; 304 | double d = diff.Length(); 305 | 306 | if (PixelLength - l < d) 307 | { 308 | calculatedPath[i + 1] = calculatedPath[i] + diff * (float)((PixelLength - l) / d); 309 | calculatedPath.RemoveRange(i + 2, calculatedPath.Count - 2 - i); 310 | 311 | l = PixelLength; 312 | cumulativeLength.Add(l); 313 | break; 314 | } 315 | 316 | l += d; 317 | cumulativeLength.Add(l); 318 | } 319 | 320 | if (l < PixelLength && calculatedPath.Count > 1) 321 | { 322 | Vector2 diff = calculatedPath[calculatedPath.Count - 1] - calculatedPath[calculatedPath.Count - 2]; 323 | double d = diff.Length(); 324 | 325 | if (d <= 0) 326 | return; 327 | 328 | calculatedPath[calculatedPath.Count - 1] += diff * (float)((PixelLength - l) / d); 329 | cumulativeLength[calculatedPath.Count - 1] = PixelLength; 330 | } 331 | } 332 | 333 | private int indexOfDistance(double d) 334 | { 335 | int i = cumulativeLength.BinarySearch(d); 336 | if (i < 0) i = ~i; 337 | 338 | return i; 339 | } 340 | 341 | private double progressToDistance(double progress) 342 | { 343 | return progress * Distance; 344 | } 345 | 346 | private Vector2 interpolateVertices(int i, double d) 347 | { 348 | if (calculatedPath.Count == 0) 349 | return Vector2.Zero; 350 | 351 | if (i <= 0) 352 | return calculatedPath.First(); 353 | if (i >= calculatedPath.Count) 354 | return calculatedPath.Last(); 355 | 356 | Vector2 p0 = calculatedPath[i - 1]; 357 | Vector2 p1 = calculatedPath[i]; 358 | 359 | double d0 = cumulativeLength[i - 1]; 360 | double d1 = cumulativeLength[i]; 361 | 362 | if (d0.AlmostEquals(d1, 1e-7)) 363 | return p0; 364 | 365 | double w = (d - d0) / (d1 - d0); 366 | return p0 + (p1 - p0) * (float)w; 367 | } 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /osu!rx/Core/Relax.cs: -------------------------------------------------------------------------------- 1 | using osu_rx.Configuration; 2 | using osu_rx.Dependencies; 3 | using osu_rx.Helpers; 4 | using osu_rx.osu; 5 | using OsuParsers.Beatmaps; 6 | using OsuParsers.Beatmaps.Objects; 7 | using OsuParsers.Enums; 8 | using System; 9 | using System.Numerics; 10 | using System.Threading; 11 | using WindowsInput; 12 | using WindowsInput.Native; 13 | 14 | namespace osu_rx.Core 15 | { 16 | public class Relax 17 | { 18 | private OsuManager osuManager; 19 | private ConfigManager configManager; 20 | private InputSimulator inputSimulator; 21 | 22 | private Beatmap currentBeatmap; 23 | private bool shouldStop; 24 | 25 | private int hitWindow50; 26 | private int hitWindow100; 27 | private int hitWindow300; 28 | 29 | private Random random = new Random(); 30 | 31 | private Timewarp timewarp; 32 | public Relax() 33 | { 34 | osuManager = DependencyContainer.Get(); 35 | configManager = DependencyContainer.Get(); 36 | inputSimulator = new InputSimulator(); 37 | timewarp = new Timewarp(); 38 | } 39 | 40 | public void Start() 41 | { 42 | shouldStop = false; 43 | currentBeatmap = postProcessBeatmap(osuManager.Player.Beatmap); 44 | 45 | hitWindow50 = osuManager.HitWindow50(currentBeatmap.DifficultySection.OverallDifficulty); 46 | hitWindow100 = osuManager.HitWindow100(currentBeatmap.DifficultySection.OverallDifficulty); 47 | hitWindow300 = osuManager.HitWindow300(currentBeatmap.DifficultySection.OverallDifficulty); 48 | 49 | float audioRate = (osuManager.CurrentMods.HasFlag(Mods.DoubleTime) || osuManager.CurrentMods.HasFlag(Mods.Nightcore)) ? 1.5f : osuManager.CurrentMods.HasFlag(Mods.HalfTime) ? 0.75f : 1f; 50 | float maxBPM = configManager.MaxSingletapBPM / (audioRate / 2); 51 | 52 | int index, lastTime, hitTime = 0; 53 | bool isHit, shouldStartAlternating, shouldAlternate; 54 | VirtualKeyCode currentKey; 55 | HitObject currentHitObject; 56 | (int StartOffset, int HoldTime) currentHitTimings; 57 | 58 | reset(); 59 | 60 | while (osuManager.CanPlay && index < currentBeatmap.HitObjects.Count && !shouldStop) 61 | { 62 | Thread.Sleep(1); 63 | 64 | if (configManager.EnableTimewarp) 65 | timewarp.Update(configManager.TimewarpRate, audioRate); 66 | 67 | if (osuManager.IsPaused) 68 | { 69 | if (isHit) 70 | { 71 | isHit = false; 72 | releaseAllKeys(); 73 | } 74 | 75 | continue; 76 | } 77 | 78 | if (lastTime > osuManager.CurrentTime) 79 | { 80 | reset(true); 81 | releaseAllKeys(); 82 | continue; 83 | } 84 | else 85 | lastTime = osuManager.CurrentTime; 86 | 87 | int currentTime = osuManager.CurrentTime + configManager.AudioOffset; 88 | if (currentTime >= currentHitObject.StartTime - hitWindow50) 89 | { 90 | if (!isHit) 91 | { 92 | var hitScanResult = getHitScanResult(index); 93 | switch (hitScanResult) 94 | { 95 | case HitScanResult.CanHit when currentTime >= currentHitObject.StartTime + currentHitTimings.StartOffset: 96 | case HitScanResult.ShouldHit: 97 | { 98 | isHit = true; 99 | hitTime = currentTime; 100 | 101 | switch (configManager.PlayStyle) 102 | { 103 | case PlayStyles.MouseOnly when currentKey == configManager.PrimaryKey: 104 | inputSimulator.Mouse.LeftButtonDown(); 105 | break; 106 | case PlayStyles.MouseOnly: 107 | inputSimulator.Mouse.RightButtonDown(); 108 | break; 109 | case PlayStyles.TapX when !shouldAlternate && !shouldStartAlternating: 110 | inputSimulator.Mouse.LeftButtonDown(); 111 | currentKey = configManager.PrimaryKey; 112 | break; 113 | default: 114 | inputSimulator.Keyboard.KeyDown(currentKey); 115 | break; 116 | } 117 | } 118 | break; 119 | case HitScanResult.MoveToNextObject: 120 | moveToNextObject(); 121 | break; 122 | } 123 | } 124 | else if (currentTime >= (currentHitObject is HitCircle ? hitTime : currentHitObject.EndTime) + currentHitTimings.HoldTime) 125 | { 126 | moveToNextObject(); 127 | 128 | if (currentHitObject is Spinner && currentHitObject.StartTime - currentBeatmap.HitObjects[index - 1].EndTime <= configManager.HoldBeforeSpinnerTime) 129 | continue; 130 | 131 | isHit = false; 132 | releaseAllKeys(); 133 | } 134 | } 135 | } 136 | 137 | releaseAllKeys(); 138 | 139 | if (configManager.EnableTimewarp) 140 | timewarp.Reset(); 141 | 142 | while (osuManager.CanPlay && index >= currentBeatmap.HitObjects.Count && !shouldStop) 143 | Thread.Sleep(5); 144 | 145 | void reset(bool retry = false) 146 | { 147 | index = retry ? 0 : closestHitObjectIndex; 148 | isHit = false; 149 | currentKey = configManager.PrimaryKey; 150 | currentHitObject = currentBeatmap.HitObjects[index]; 151 | updateAlternate(); 152 | currentHitTimings = randomizeHitObjectTimings(index, shouldAlternate, false); 153 | lastTime = int.MinValue; 154 | 155 | if (configManager.EnableTimewarp) 156 | timewarp.Refresh(); 157 | } 158 | 159 | void updateAlternate() 160 | { 161 | var lastHitObject = index > 0 ? currentBeatmap.HitObjects[index - 1] : null; 162 | var nextHitObject = index + 1 < currentBeatmap.HitObjects.Count ? currentBeatmap.HitObjects[index + 1] : null; 163 | 164 | shouldStartAlternating = nextHitObject != null ? 60000 / (nextHitObject.StartTime - currentHitObject.EndTime) >= maxBPM : false; 165 | shouldAlternate = lastHitObject != null ? 60000 / (currentHitObject.StartTime - lastHitObject.EndTime) >= maxBPM : false; 166 | if (shouldAlternate || configManager.PlayStyle == PlayStyles.Alternate) 167 | currentKey = (currentKey == configManager.PrimaryKey) ? configManager.SecondaryKey : configManager.PrimaryKey; 168 | else 169 | currentKey = configManager.PrimaryKey; 170 | } 171 | 172 | void moveToNextObject() 173 | { 174 | index++; 175 | if (index < currentBeatmap.HitObjects.Count) 176 | { 177 | currentHitObject = currentBeatmap.HitObjects[index]; 178 | 179 | updateAlternate(); 180 | currentHitTimings = randomizeHitObjectTimings(index, shouldAlternate, inputSimulator.InputDeviceState.IsKeyDown(configManager.HitWindow100Key)); 181 | } 182 | } 183 | } 184 | 185 | public void Stop() => shouldStop = true; 186 | 187 | private Beatmap postProcessBeatmap(Beatmap beatmap) 188 | { 189 | foreach (var hitObject in beatmap.HitObjects) 190 | if (hitObject is Slider slider) 191 | for (int i = 0; i < slider.SliderPoints.Count; i++) 192 | slider.SliderPoints[i] -= slider.Position; 193 | 194 | return beatmap; 195 | } 196 | 197 | private void releaseAllKeys() 198 | { 199 | inputSimulator.Keyboard.KeyUp(configManager.PrimaryKey); 200 | inputSimulator.Keyboard.KeyUp(configManager.SecondaryKey); 201 | inputSimulator.Mouse.LeftButtonUp(); 202 | inputSimulator.Mouse.RightButtonUp(); 203 | } 204 | 205 | private int closestHitObjectIndex 206 | { 207 | get 208 | { 209 | int time = osuManager.CurrentTime; 210 | for (int i = 0; i < currentBeatmap.HitObjects.Count; i++) 211 | if (currentBeatmap.HitObjects[i].StartTime >= time) 212 | return i; 213 | 214 | return currentBeatmap.HitObjects.Count; 215 | } 216 | } 217 | 218 | private int lastHitScanIndex = -1; 219 | private Vector2? lastOnNotePosition = null; 220 | private SliderPath lastSliderPath = null; 221 | private HitScanResult getHitScanResult(int index) 222 | { 223 | var hitObject = currentBeatmap.HitObjects[index]; 224 | 225 | if (!configManager.EnableHitScan || hitObject is Spinner) 226 | return HitScanResult.CanHit; 227 | 228 | if (lastHitScanIndex != index) 229 | { 230 | lastHitScanIndex = index; 231 | lastOnNotePosition = null; 232 | lastSliderPath = hitObject is Slider slider ? new SliderPath(slider) : null; 233 | } 234 | 235 | float hitObjectRadius = osuManager.HitObjectRadius(currentBeatmap.DifficultySection.CircleSize); 236 | Vector2 hitObjectPosition = hitObject.Position; 237 | 238 | if (hitObject is Slider && osuManager.CurrentTime > hitObject.StartTime) 239 | { 240 | float sliderDuration = hitObject.EndTime - hitObject.StartTime; 241 | float currentSliderTime = osuManager.CurrentTime - hitObject.StartTime; 242 | double progress = (currentSliderTime / sliderDuration).Clamp(0, 1) * (hitObject as Slider).Repeats % 1; 243 | progress = lastSliderPath.ProgressAt(progress); 244 | 245 | hitObjectPosition += lastSliderPath.PositionAt(progress); 246 | } 247 | 248 | float distanceToObject = Vector2.Distance(osuManager.CursorPosition, hitObjectPosition * osuManager.OsuWindow.PlayfieldRatio); 249 | float distanceToLastPos = Vector2.Distance(osuManager.CursorPosition, lastOnNotePosition ?? Vector2.Zero); 250 | 251 | if (osuManager.CurrentTime > hitObject.EndTime + hitWindow50) 252 | { 253 | if (configManager.HitScanMissAfterHitWindow50) 254 | if (distanceToObject <= hitObjectRadius + configManager.HitScanRadiusAdditional && !intersectsWithOtherHitObjects(index + 1)) 255 | return HitScanResult.ShouldHit; 256 | 257 | return HitScanResult.MoveToNextObject; 258 | } 259 | 260 | if (configManager.EnableHitScanPrediction) 261 | { 262 | if (distanceToObject > hitObjectRadius * configManager.HitScanRadiusMultiplier) 263 | { 264 | if (lastOnNotePosition != null && distanceToLastPos <= configManager.HitScanMaxDistance) 265 | return HitScanResult.ShouldHit; 266 | } 267 | else 268 | lastOnNotePosition = osuManager.CursorPosition; 269 | } 270 | 271 | if (distanceToObject <= hitObjectRadius) 272 | return HitScanResult.CanHit; 273 | 274 | if (configManager.HitScanMissChance != 0) 275 | if (distanceToObject <= hitObjectRadius + configManager.HitScanRadiusAdditional && random.Next(1, 101) <= configManager.HitScanMissChance && !intersectsWithOtherHitObjects(index + 1)) 276 | return HitScanResult.CanHit; 277 | 278 | return HitScanResult.Wait; 279 | } 280 | 281 | private (int StartOffset, int HoldTime) randomizeHitObjectTimings(int index, bool alternating, bool allowHit100) 282 | { 283 | (int StartOffset, int HoldTime) result; 284 | 285 | float acc = alternating ? random.NextFloat(1.2f, 1.7f) : 2; 286 | 287 | if (allowHit100) 288 | result.StartOffset = random.Next(-hitWindow100 / 2, hitWindow100 / 2); 289 | else 290 | result.StartOffset = random.Next((int)(-hitWindow300 / acc), (int)(hitWindow300 / acc)); 291 | 292 | if (currentBeatmap.HitObjects[index] is Slider) 293 | { 294 | int sliderDuration = currentBeatmap.HitObjects[index].EndTime - currentBeatmap.HitObjects[index].StartTime; 295 | result.HoldTime = random.Next(sliderDuration >= 72 ? -26 : sliderDuration / 2 - 10, hitWindow300 * 2); 296 | } 297 | else 298 | result.HoldTime = random.Next(hitWindow300, hitWindow300 * 2); 299 | 300 | return result; 301 | } 302 | 303 | private bool intersectsWithOtherHitObjects(int startIndex) 304 | { 305 | int time = osuManager.CurrentTime; 306 | Vector2 cursorPosition = osuManager.CursorPosition; 307 | 308 | for (int i = startIndex; i < currentBeatmap.HitObjects.Count; i++) 309 | { 310 | var hitObject = currentBeatmap.HitObjects[i]; 311 | double preEmpt = osuManager.DifficultyRange(currentBeatmap.DifficultySection.ApproachRate, 1800, 1200, 450); 312 | double startTime = hitObject.StartTime - preEmpt; 313 | if (startTime > time) 314 | break; 315 | 316 | float distanceToObject = Vector2.Distance(cursorPosition, hitObject.Position * osuManager.OsuWindow.PlayfieldRatio); 317 | if (distanceToObject <= osuManager.HitObjectRadius(currentBeatmap.DifficultySection.CircleSize)) 318 | return true; 319 | } 320 | 321 | return false; 322 | } 323 | } 324 | } -------------------------------------------------------------------------------- /osu!rx/Program.cs: -------------------------------------------------------------------------------- 1 | using osu_rx.Configuration; 2 | using osu_rx.Core; 3 | using osu_rx.Dependencies; 4 | using osu_rx.osu; 5 | using OsuParsers.Enums; 6 | using System; 7 | using System.Reflection; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using WindowsInput.Native; 11 | 12 | namespace osu_rx 13 | { 14 | class Program 15 | { 16 | private static OsuManager osuManager; 17 | private static ConfigManager configManager; 18 | private static Relax relax; 19 | private static string defaultConsoleTitle; 20 | 21 | static void Main(string[] args) 22 | { 23 | osuManager = new OsuManager(); 24 | 25 | if (!osuManager.Initialize()) 26 | { 27 | Console.WriteLine(); 28 | Console.WriteLine("osu!rx will close in 5 seconds..."); 29 | Thread.Sleep(5000); 30 | Environment.Exit(0); 31 | } 32 | 33 | configManager = new ConfigManager(); 34 | 35 | DependencyContainer.Cache(osuManager); 36 | DependencyContainer.Cache(configManager); 37 | 38 | relax = new Relax(); 39 | 40 | defaultConsoleTitle = Console.Title; 41 | if (configManager.UseCustomWindowTitle) 42 | Console.Title = configManager.CustomWindowTitle; 43 | 44 | DrawMainMenu(); 45 | } 46 | 47 | private static void DrawMainMenu() 48 | { 49 | string version = Assembly.GetExecutingAssembly().GetName().Version.ToString(); 50 | version = version.Remove(version.LastIndexOf(".0")); 51 | 52 | Console.Clear(); 53 | Console.WriteLine($"osu!rx v{version} (MPGH release){(osuManager.UsingIPCFallback ? " | [IPC Fallback mode]" : string.Empty)}"); 54 | Console.WriteLine("\n---Main Menu---"); 55 | Console.WriteLine("\n1. Start relax"); 56 | Console.WriteLine("2. Settings"); 57 | 58 | if (osuManager.UsingIPCFallback) 59 | Console.WriteLine("\n3. What is IPC Fallback mode?"); 60 | 61 | switch (Console.ReadKey(true).Key) 62 | { 63 | case ConsoleKey.D1: 64 | StartRelax(); 65 | break; 66 | case ConsoleKey.D2: 67 | DrawSettings(); 68 | break; 69 | case ConsoleKey.D3: 70 | if (osuManager.UsingIPCFallback) 71 | DrawIPCFallbackInfo(); 72 | else 73 | DrawMainMenu(); 74 | break; 75 | default: 76 | DrawMainMenu(); 77 | break; 78 | } 79 | } 80 | 81 | private static void DrawSettings() 82 | { 83 | Console.Clear(); 84 | Console.WriteLine("---Settings---\n"); 85 | Console.WriteLine("1. Relax settings"); 86 | Console.WriteLine("2. HitScan settings"); 87 | Console.WriteLine("3. Other settings\n"); 88 | Console.WriteLine("4. Experimental settings"); 89 | 90 | Console.WriteLine("\nESC. Back to main menu"); 91 | 92 | switch (Console.ReadKey(true).Key) 93 | { 94 | case ConsoleKey.D1: 95 | DrawRelaxSettings(); 96 | break; 97 | case ConsoleKey.D2: 98 | DrawHitScanSettings(); 99 | break; 100 | case ConsoleKey.D3: 101 | DrawOtherSettings(); 102 | break; 103 | case ConsoleKey.D4: 104 | DrawExperimentalSettings(); 105 | break; 106 | case ConsoleKey.Escape: 107 | DrawMainMenu(); 108 | break; 109 | default: 110 | DrawSettings(); 111 | break; 112 | } 113 | } 114 | 115 | private static void DrawRelaxSettings() 116 | { 117 | Console.Clear(); 118 | Console.WriteLine("---Relax Settings---\n"); 119 | Console.WriteLine($"1. Playstyle | [{configManager.PlayStyle}]"); 120 | Console.WriteLine($"2. Primary key | [{configManager.PrimaryKey}]"); 121 | Console.WriteLine($"3. Secondary key | [{configManager.SecondaryKey}]"); 122 | Console.WriteLine($"4. Hit window 100 key | [{configManager.HitWindow100Key}]"); 123 | Console.WriteLine($"5. Max singletap BPM | [{configManager.MaxSingletapBPM}]"); 124 | Console.WriteLine($"6. Audio offset | [{configManager.AudioOffset}]"); 125 | Console.WriteLine($"7. HoldBeforeSpinner time | [{configManager.HoldBeforeSpinnerTime}]"); 126 | 127 | Console.WriteLine("\nESC. Back to settings"); 128 | 129 | switch (Console.ReadKey(true).Key) 130 | { 131 | case ConsoleKey.D1: 132 | Console.Clear(); 133 | Console.WriteLine("Select new playstyle:\n"); 134 | PlayStyles[] playstyles = (PlayStyles[])Enum.GetValues(typeof(PlayStyles)); 135 | for (int i = 0; i < playstyles.Length; i++) 136 | Console.WriteLine($"{i + 1}. {playstyles[i]}"); 137 | if (int.TryParse(Console.ReadKey(true).KeyChar.ToString(), out int selected) && selected > 0 && selected < 5) 138 | configManager.PlayStyle = (PlayStyles)selected - 1; 139 | else 140 | goto case ConsoleKey.D1; 141 | DrawRelaxSettings(); 142 | break; 143 | case ConsoleKey.D2: 144 | Console.Clear(); 145 | Console.Write("Enter new primary key: "); 146 | configManager.PrimaryKey = (VirtualKeyCode)Console.ReadKey(true).Key; 147 | DrawRelaxSettings(); 148 | break; 149 | case ConsoleKey.D3: 150 | Console.Clear(); 151 | Console.Write("Enter new secondary key: "); 152 | configManager.SecondaryKey = (VirtualKeyCode)Console.ReadKey(true).Key; 153 | DrawRelaxSettings(); 154 | break; 155 | case ConsoleKey.D4: 156 | Console.Clear(); 157 | Console.Write("Enter new hit window 100 key: "); 158 | configManager.HitWindow100Key = (VirtualKeyCode)Console.ReadKey(true).Key; 159 | DrawRelaxSettings(); 160 | break; 161 | case ConsoleKey.D5: 162 | Console.Clear(); 163 | Console.Write("Enter new max singletap BPM: "); 164 | if (int.TryParse(Console.ReadLine(), out int bpm)) 165 | configManager.MaxSingletapBPM = bpm; 166 | else 167 | goto case ConsoleKey.D5; 168 | DrawRelaxSettings(); 169 | break; 170 | case ConsoleKey.D6: 171 | Console.Clear(); 172 | Console.Write("Enter new audio offset: "); 173 | if (int.TryParse(Console.ReadLine(), out int offset)) 174 | configManager.AudioOffset = offset; 175 | else 176 | goto case ConsoleKey.D6; 177 | DrawRelaxSettings(); 178 | break; 179 | case ConsoleKey.D7: 180 | Console.Clear(); 181 | Console.Write("Enter new HoldBeforeSpinner time: "); 182 | if (int.TryParse(Console.ReadLine(), out int holdBeforeSpinnerTime)) 183 | configManager.HoldBeforeSpinnerTime = holdBeforeSpinnerTime; 184 | else 185 | goto case ConsoleKey.D7; 186 | DrawRelaxSettings(); 187 | break; 188 | case ConsoleKey.Escape: 189 | DrawSettings(); 190 | break; 191 | default: 192 | DrawRelaxSettings(); 193 | break; 194 | } 195 | } 196 | 197 | private static void DrawHitScanSettings() 198 | { 199 | Console.Clear(); 200 | Console.WriteLine("---HitScan Settings---\n"); 201 | Console.WriteLine($"1. HitScan | [{(configManager.EnableHitScan ? "ENABLED" : "DISABLED")}]"); 202 | Console.WriteLine($"2. Prediction | [{(configManager.EnableHitScanPrediction ? "ENABLED" : "DISABLED")}]"); 203 | Console.WriteLine($"3. Radius multiplier | [{configManager.HitScanRadiusMultiplier}]"); 204 | Console.WriteLine($"4. Radius additional | [{configManager.HitScanRadiusAdditional}]"); 205 | Console.WriteLine($"5. Max distance | [{configManager.HitScanMaxDistance}]"); 206 | Console.WriteLine($"6. Miss chance | [{configManager.HitScanMissChance}%]"); 207 | Console.WriteLine($"7. Miss after HitWindow50 | [{(configManager.HitScanMissAfterHitWindow50 ? "ENABLED" : "DISABLED")}]"); 208 | 209 | Console.WriteLine("\nESC. Back to settings"); 210 | 211 | switch (Console.ReadKey(true).Key) 212 | { 213 | case ConsoleKey.D1: 214 | configManager.EnableHitScan = !configManager.EnableHitScan; 215 | DrawHitScanSettings(); 216 | break; 217 | case ConsoleKey.D2: 218 | configManager.EnableHitScanPrediction = !configManager.EnableHitScanPrediction; 219 | DrawHitScanSettings(); 220 | break; 221 | case ConsoleKey.D3: 222 | Console.Clear(); 223 | Console.Write("Enter new radius multiplier: "); 224 | if (float.TryParse(Console.ReadLine(), out float multiplier)) 225 | configManager.HitScanRadiusMultiplier = multiplier; 226 | else 227 | goto case ConsoleKey.D3; 228 | DrawHitScanSettings(); 229 | break; 230 | case ConsoleKey.D4: 231 | Console.Clear(); 232 | Console.Write("Enter new radius additional: "); 233 | if (int.TryParse(Console.ReadLine(), out int additional)) 234 | configManager.HitScanRadiusAdditional = additional; 235 | else 236 | goto case ConsoleKey.D4; 237 | DrawHitScanSettings(); 238 | break; 239 | case ConsoleKey.D5: 240 | Console.Clear(); 241 | Console.Write("Enter new max distance: "); 242 | if (int.TryParse(Console.ReadLine(), out int maxDistance)) 243 | configManager.HitScanMaxDistance = maxDistance; 244 | else 245 | goto case ConsoleKey.D5; 246 | DrawHitScanSettings(); 247 | break; 248 | case ConsoleKey.D6: 249 | Console.Clear(); 250 | Console.Write("Enter new miss chance: "); 251 | if (int.TryParse(Console.ReadLine(), out int missChance)) 252 | configManager.HitScanMissChance = missChance; 253 | else 254 | goto case ConsoleKey.D6; 255 | DrawHitScanSettings(); 256 | break; 257 | case ConsoleKey.D7: 258 | configManager.HitScanMissAfterHitWindow50 = !configManager.HitScanMissAfterHitWindow50; 259 | DrawHitScanSettings(); 260 | break; 261 | case ConsoleKey.Escape: 262 | DrawSettings(); 263 | break; 264 | default: 265 | DrawHitScanSettings(); 266 | break; 267 | } 268 | } 269 | 270 | private static void DrawOtherSettings() 271 | { 272 | Console.Clear(); 273 | Console.WriteLine("---Other Settings---\n"); 274 | Console.WriteLine($"1. Custom window title | [{(configManager.UseCustomWindowTitle ? $"ON | {configManager.CustomWindowTitle}" : "OFF")}]"); 275 | 276 | if (!osuManager.UsingIPCFallback) 277 | Console.WriteLine("\n\n0. Turn on IPC Fallback mode"); 278 | 279 | Console.WriteLine("\nESC. Back to settings"); 280 | 281 | switch (Console.ReadKey(true).Key) 282 | { 283 | case ConsoleKey.D1: 284 | Console.Clear(); 285 | Console.WriteLine("Use custom window title?\n"); 286 | Console.WriteLine("1. Yes"); 287 | Console.WriteLine("2. No"); 288 | configManager.UseCustomWindowTitle = Console.ReadKey(true).Key == ConsoleKey.D1; 289 | if (configManager.UseCustomWindowTitle) 290 | { 291 | Console.Clear(); 292 | Console.Write("Enter new custom window title: "); 293 | configManager.CustomWindowTitle = Console.ReadLine(); 294 | Console.Title = configManager.CustomWindowTitle; 295 | } 296 | else 297 | Console.Title = defaultConsoleTitle; 298 | DrawOtherSettings(); 299 | break; 300 | case ConsoleKey.D0: 301 | if (!osuManager.UsingIPCFallback) 302 | { 303 | Console.Clear(); 304 | Console.WriteLine("Turn this on manually only if osu!rx works incorrectly for you."); 305 | Console.WriteLine("You won't be able to turn off IPC Fallback mode without restarting osu!rx."); 306 | Console.WriteLine("\nTurn on IPC Fallback mode anyway?"); 307 | Console.WriteLine("\n1. Yes"); 308 | Console.WriteLine("2. No"); 309 | 310 | osuManager.UsingIPCFallback = Console.ReadKey(true).Key == ConsoleKey.D1; 311 | DrawOtherSettings(); 312 | } 313 | else 314 | DrawOtherSettings(); 315 | break; 316 | case ConsoleKey.Escape: 317 | DrawSettings(); 318 | break; 319 | default: 320 | DrawOtherSettings(); 321 | break; 322 | } 323 | } 324 | 325 | private static void DrawExperimentalSettings() 326 | { 327 | Console.Clear(); 328 | Console.WriteLine("---Experimental Settings---\n"); 329 | Console.WriteLine($"1. Timewarp | [{(configManager.EnableTimewarp ? "ENABLED" : "DISABLED")}]"); 330 | Console.WriteLine($"2. Timewarp rate | [{configManager.TimewarpRate}x]"); 331 | 332 | Console.WriteLine("\nESC. Back to settings"); 333 | 334 | switch (Console.ReadKey(true).Key) 335 | { 336 | case ConsoleKey.D1: 337 | configManager.EnableTimewarp = !configManager.EnableTimewarp; 338 | DrawExperimentalSettings(); 339 | break; 340 | case ConsoleKey.D2: 341 | Console.Clear(); 342 | Console.Write("Enter new timewarp rate: "); 343 | if (double.TryParse(Console.ReadLine(), out double rate)) 344 | configManager.TimewarpRate = rate; 345 | else 346 | goto case ConsoleKey.D2; 347 | DrawExperimentalSettings(); 348 | break; 349 | case ConsoleKey.Escape: 350 | DrawSettings(); 351 | break; 352 | default: 353 | DrawExperimentalSettings(); 354 | break; 355 | } 356 | } 357 | 358 | private static void StartRelax() 359 | { 360 | bool shouldExit = false; 361 | Task.Run(() => 362 | { 363 | while (Console.ReadKey(true).Key != ConsoleKey.Escape) ; 364 | 365 | shouldExit = true; 366 | relax.Stop(); 367 | }); 368 | 369 | while (!shouldExit) 370 | { 371 | Console.Clear(); 372 | Console.WriteLine("Idling"); 373 | Console.WriteLine("\nPress ESC to return to the main menu."); 374 | 375 | while (!osuManager.CanPlay && !shouldExit) 376 | Thread.Sleep(5); 377 | 378 | if (shouldExit) 379 | break; 380 | 381 | var beatmap = osuManager.Player.Beatmap; 382 | if (beatmap.GeneralSection.Mode != Ruleset.Standard) 383 | { 384 | Console.Clear(); 385 | Console.WriteLine("Only osu!standard beatmaps are supported!\n\nReturn to song select to continue or press ESC to return to main menu."); 386 | 387 | while (osuManager.CanPlay && !shouldExit) 388 | Thread.Sleep(1); 389 | 390 | if (shouldExit) 391 | break; 392 | 393 | continue; 394 | } 395 | 396 | Console.Clear(); 397 | Console.WriteLine($"Playing {beatmap.MetadataSection.Artist} - {beatmap.MetadataSection.Title} ({beatmap.MetadataSection.Creator}) [{beatmap.MetadataSection.Version}]"); 398 | Console.WriteLine("\nPress ESC to return to the main menu."); 399 | 400 | relax.Start(); 401 | } 402 | 403 | DrawMainMenu(); 404 | } 405 | 406 | private static void DrawIPCFallbackInfo() 407 | { 408 | Console.Clear(); 409 | Console.WriteLine("---What is IPC Fallback mode?---"); 410 | Console.WriteLine("\nIPC Fallback mode automatically turns on when osu!rx fails to find important addresses in game's memory."); 411 | Console.WriteLine("While IPC Fallback mode is on, osu!rx will communicate with osu! through IPC to get needed variables."); 412 | Console.WriteLine("That means you can use osu!rx even if it's outdated."); 413 | Console.WriteLine("\nHowever, IPC Fallback mode has quite a few cons:"); 414 | Console.WriteLine("\n1. You will experience timing issues if your game is running below 200 fps."); 415 | Console.WriteLine("2. You will probably be missing a lot if your game lags/stutters."); 416 | Console.WriteLine("3. Mods support will no longer work.\n That means you won't be able to play hd/ez mods with hitscan and your max singletap bpm won't scale with dt/ht mods."); 417 | Console.WriteLine("4. Hitscan may not work if you have raw input turned on."); 418 | Console.WriteLine("5. And you'll probably experience a bunch of other unknown issues."); 419 | Console.WriteLine("\nBut hey, you'll still be able to use osu!rx (in some way) even if i die and won't be able to update this piece of junk ;)"); 420 | Console.WriteLine("\n---How to get rid of this?---"); 421 | Console.WriteLine("\n- Try restarting osu! and osu!rx multiple times."); 422 | Console.WriteLine("- If the advice above didn't helped, then report this on github/mpgh and i'll try to fix this ASAP!"); 423 | Console.WriteLine("\nPress ESC to return to the main menu."); 424 | 425 | if (Console.ReadKey(true).Key == ConsoleKey.Escape) 426 | DrawMainMenu(); 427 | else 428 | DrawIPCFallbackInfo(); 429 | } 430 | } 431 | } 432 | --------------------------------------------------------------------------------