├── .assets └── logo.svg ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Qsor.Deploy ├── ChangelogGenerator.cs ├── Program.cs ├── Qsor.Deploy.csproj └── tools │ └── rcedit-x64.exe ├── Qsor.Desktop ├── Program.cs ├── Qsor.Desktop.csproj ├── QsorGameDesktop.cs ├── Updater │ └── SquirrelUpdateManager.cs ├── icon.ico └── qsor.nuspec ├── Qsor.Game ├── Beatmaps │ ├── Beatmap.cs │ ├── BeatmapContainer.cs │ ├── BeatmapManager.cs │ ├── BeatmapParser.cs │ ├── TimingPoint.cs │ └── WorkingBeatmap.cs ├── Configuration │ └── QsorConfiguration.cs ├── Database │ ├── DatabaseWriteUsage.cs │ ├── Migrations │ │ ├── 20200331222405_BeatmapModel.Designer.cs │ │ ├── 20200331222405_BeatmapModel.cs │ │ └── QsorDbContextModelSnapshot.cs │ ├── Models │ │ └── BeatmapModel.cs │ ├── QsorDbContext.cs │ ├── QsorDbContextFactory.cs │ └── StorageExtension.cs ├── Graphics │ ├── ClickableSpriteIcon.cs │ ├── Containers │ │ ├── BackgroundImageContainer.cs │ │ ├── BeatSyncedContainer.cs │ │ ├── ParallaxContainer.cs │ │ └── QsorTooltipContainer.cs │ ├── DragableProgressbar.cs │ ├── Drawables │ │ └── DrawableProgressbar.cs │ └── UserInterface │ │ ├── Online │ │ └── Drawables │ │ │ ├── DrawableAvatar.cs │ │ │ └── DrawableLevelBar.cs │ │ ├── Overlays │ │ ├── MusicPlayerOverlay.cs │ │ ├── Notification │ │ │ ├── Drawables │ │ │ │ ├── DrawableBigNotification.cs │ │ │ │ └── DrawableNotification.cs │ │ │ └── NotificationOverlay.cs │ │ ├── Settings │ │ │ ├── Categories │ │ │ │ ├── SettingsAudioCategory.cs │ │ │ │ ├── SettingsEditorCategory.cs │ │ │ │ ├── SettingsGameplayCategory.cs │ │ │ │ ├── SettingsGeneralCategory.cs │ │ │ │ ├── SettingsGraphicsCategory.cs │ │ │ │ ├── SettingsInputCategory.cs │ │ │ │ ├── SettingsMaintenanceCategory.cs │ │ │ │ ├── SettingsOnlineCategory.cs │ │ │ │ └── SettingsSkinCategory.cs │ │ │ ├── Drawables │ │ │ │ ├── DrawableSettingsCategory.cs │ │ │ │ ├── DrawableSettingsIconSprite.cs │ │ │ │ ├── DrawableSettingsMenu.cs │ │ │ │ ├── DrawableSettingsSubCategory.cs │ │ │ │ ├── DrawableSettingsToolBar.cs │ │ │ │ └── Objects │ │ │ │ │ ├── DrawableSettingsCheckbox.cs │ │ │ │ │ ├── DrawableSettingsInput.cs │ │ │ │ │ ├── DrawableSettingsLabel.cs │ │ │ │ │ └── DrawableSettingsObject.cs │ │ │ ├── SettingsCategoryContainer.cs │ │ │ └── SettingsOverlay.cs │ │ ├── UpdaterOverlay.cs │ │ └── UserOverlay.cs │ │ ├── QsorColours.cs │ │ └── Screens │ │ ├── IntroScreen.cs │ │ └── MainMenu │ │ ├── Containers │ │ ├── BottomBar.cs │ │ └── Toolbar.cs │ │ ├── Drawables │ │ ├── DrawableLogoVisualisation.cs │ │ ├── DrawableMenuSideFlashes.cs │ │ └── DrawableQsorLogo.cs │ │ └── MainMenuScreen.cs ├── Input │ └── KeyBindingInputHandler.cs ├── Online │ ├── BanchoClient.cs │ ├── BeatmapMirrorAccess.cs │ ├── UserManager.cs │ └── Users │ │ ├── User.cs │ │ └── UserStatistics.cs ├── Qsor.Game.csproj ├── QsorBaseGame.cs ├── QsorGame.cs ├── Resources │ └── Textures │ │ ├── Logo-256x256.png │ │ ├── Logo-ghost.png │ │ ├── Logo.png │ │ ├── approachcircle.png │ │ ├── hitcircle.png │ │ ├── hitcircleoverlay.png │ │ ├── slider.png │ │ └── sliderb.png ├── Updater │ ├── DummyUpdateManager.cs │ ├── UpdateManager.cs │ └── UpdaterStatus.cs └── Utility │ └── SentryLogger.cs ├── Qsor.NativeLibs ├── Qsor.NativeLibs.csproj └── _._ ├── Qsor.Tests ├── Program.cs ├── Qsor.Tests.csproj ├── QsorTestGame.cs └── Visual │ ├── Overlays │ ├── TestSceneMusicPlayerOverlay.cs │ ├── TestSceneNotificationOverlay.cs │ ├── TestSceneSettingsOverlay.cs │ ├── TestSceneUpdaterOverlay.cs │ └── TestSceneUserOverlay.cs │ ├── Scenes │ └── IntroTestScene.cs │ └── TestSceneQsorGame.cs ├── Qsor.sln ├── Qsor.sln.DotSettings ├── Readme.md ├── appveyor.yml ├── generate_changelog.ps1 └── global.json /.assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.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 | # Coverlet is a free, cross platform Code Coverage Tool 141 | coverage*[.json, .xml, .info] 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ 354 | 355 | .idea/ 356 | 357 | releases/ 358 | 359 | staging/ 360 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at me@mempler.de. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Robin A. P. 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 | -------------------------------------------------------------------------------- /Qsor.Deploy/ChangelogGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using LibGit2Sharp; 4 | 5 | namespace Qsor.Deploy 6 | { 7 | public static class ChangelogGenerator 8 | { 9 | public static string GenerateChangelog() 10 | { 11 | var repoPath = Repository.Discover("."); 12 | var repo = new Repository(repoPath); 13 | 14 | var changeLog = string.Empty; 15 | 16 | foreach (var commit in repo.Commits) 17 | { 18 | var lastTag = repo.Tags.Last(); 19 | if (commit.Sha == lastTag.Target.Sha) 20 | break; 21 | 22 | changeLog += $"`{commit.Sha.Remove(7)}` {commit.MessageShort} - {commit.Author.Name} {Environment.NewLine}"; 23 | } 24 | 25 | return changeLog; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Qsor.Deploy/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using Newtonsoft.Json; 7 | using osu.Framework.IO.Network; 8 | using osu.Framework.Platform; 9 | 10 | namespace Qsor.Deploy 11 | { 12 | internal static class Program 13 | { 14 | private static string NugetPackages => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), @".nuget\packages"); 15 | private static string NugetPath => Path.Combine(NugetPackages, @"nuget.commandline\5.6.0\tools\NuGet.exe"); 16 | private static string SquirrelPath => Path.Combine(NugetPackages, @"ppy.squirrel.windows\1.9.0.4\tools\Squirrel.exe"); 17 | 18 | private static string GithubAccessToken => Environment.GetEnvironmentVariable("GITHUB_ACCESS_TOKEN"); 19 | 20 | private static void Main() 21 | { 22 | var currentDirectory = new NativeStorage("."); 23 | var solutionDirectory = new NativeStorage(".."); 24 | 25 | currentDirectory.DeleteDirectory("./staging"); 26 | 27 | if (!Directory.Exists("releases")) 28 | Directory.CreateDirectory("releases"); 29 | 30 | var stagingDirectory = currentDirectory.GetStorageForDirectory("staging"); 31 | var currentDate = DateTime.Now.ToString("yyyy.Mdd."); 32 | 33 | currentDate += currentDirectory.GetDirectories("releases").Count(s => s.Contains(currentDate)); 34 | 35 | var releaseDirectory = currentDirectory.GetStorageForDirectory($"./releases/App-{currentDate}"); 36 | 37 | Console.WriteLine($"Package: Qsor"); 38 | Console.WriteLine($"Release Version: {currentDate}"); 39 | Console.WriteLine($"Release Directory: {releaseDirectory.GetFullPath(".")}"); 40 | Console.WriteLine($"Changelog: \n{ChangelogGenerator.GenerateChangelog()}"); 41 | 42 | var logo = solutionDirectory.GetFullPath("Qsor.Game/Resources/Textures/Logo-256x256.png"); 43 | var icon = solutionDirectory.GetFullPath("Qsor.Desktop/icon.ico"); 44 | 45 | RunCommand("dotnet", $"publish -f net5.0 Qsor.Desktop --configuration Release --runtime win-x64 -p:Version={currentDate} -o {stagingDirectory.GetFullPath(".")}", 46 | solutionDirectory.GetFullPath(".")); 47 | 48 | RunCommand("./tools/rcedit-x64.exe", $"\"{stagingDirectory.GetFullPath(".")}\\Qsor.exe\" --set-icon \"{icon}\""); 49 | 50 | RunCommand(NugetPath, $"pack Qsor.Desktop/qsor.nuspec -Version {currentDate} -Properties Configuration=Release -OutputDirectory {stagingDirectory.GetFullPath(".")} -BasePath {stagingDirectory.GetFullPath(".")}", 51 | solutionDirectory.GetFullPath(".")); 52 | 53 | RunCommand(SquirrelPath, $"--releasify {stagingDirectory.GetFullPath($"./Qsor.{currentDate}.nupkg")} --releaseDir {releaseDirectory.GetFullPath(".")} --no-msi --icon {icon} --setupIcon {icon} --loadingGif {logo}", 54 | stagingDirectory.GetFullPath(".")); 55 | 56 | RunCommand("git", $"tag {currentDate}"); 57 | RunCommand("git", $"push origin {currentDate}"); 58 | 59 | File.Move(releaseDirectory.GetFullPath("Setup.exe"), releaseDirectory.GetFullPath("install.exe")); 60 | 61 | stagingDirectory.DeleteDirectory("."); 62 | 63 | var req = new JsonWebRequest($"https://api.github.com/repos/mempler/Qsor/releases") 64 | { 65 | Method = HttpMethod.Post, 66 | }; 67 | 68 | Console.WriteLine($"Creating release {currentDate}..."); 69 | 70 | req.AddRaw(JsonConvert.SerializeObject(new GitHubRelease 71 | { 72 | Name = currentDate, 73 | Draft = true, 74 | Body = ChangelogGenerator.GenerateChangelog() 75 | })); 76 | 77 | req.AddHeader("Authorization", $"token {GithubAccessToken}"); 78 | req.Perform(); 79 | 80 | var targetRelease = req.ResponseObject; 81 | 82 | var assetUploadUrl = targetRelease.UploadUrl.Replace("{?name,label}", "?name={0}"); 83 | foreach (var a in Directory.GetFiles(releaseDirectory.GetFullPath(".")).Reverse()) 84 | { 85 | if (Path.GetFileName(a).StartsWith('.')) 86 | continue; 87 | 88 | Console.WriteLine($"- Pushing asset {a}..."); 89 | var upload = new WebRequest(assetUploadUrl, Path.GetFileName(a)) 90 | { 91 | Method = HttpMethod.Post, 92 | Timeout = 240000, 93 | ContentType = "application/octet-stream", 94 | }; 95 | 96 | upload.AddRaw(File.ReadAllBytes(a)); 97 | upload.AddHeader("Authorization", $"token {GithubAccessToken}"); 98 | upload.Perform(); 99 | } 100 | } 101 | 102 | private static void RunCommand(string command, string args, string workingDirectory = null) 103 | { 104 | Console.WriteLine($"Running {command} {args}..."); 105 | 106 | var psi = new ProcessStartInfo(command, args) 107 | { 108 | WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory, 109 | CreateNoWindow = true, 110 | 111 | RedirectStandardOutput = true, 112 | RedirectStandardError = true, 113 | 114 | UseShellExecute = false, 115 | WindowStyle = ProcessWindowStyle.Hidden 116 | }; 117 | 118 | var p = Process.Start(psi); 119 | if (p == null) return; 120 | 121 | var output = p.StandardOutput.ReadToEnd() + p.StandardError.ReadToEnd(); 122 | 123 | p.WaitForExit(); 124 | 125 | if (p.ExitCode == 0) return; 126 | 127 | Console.WriteLine(output); 128 | Console.WriteLine($"Command {command} {args} failed!"); 129 | } 130 | } 131 | 132 | public class GitHubRelease 133 | { 134 | [JsonProperty(@"id")] 135 | public int Id; 136 | 137 | [JsonProperty(@"tag_name")] 138 | public string TagName => $"{Name}"; 139 | 140 | [JsonProperty(@"name")] 141 | public string Name; 142 | 143 | [JsonProperty(@"draft")] 144 | public bool Draft; 145 | 146 | [JsonProperty(@"prerelease")] 147 | public bool PreRelease; 148 | 149 | [JsonProperty(@"upload_url")] 150 | public string UploadUrl; 151 | 152 | [JsonProperty(@"body")] 153 | public string Body; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Qsor.Deploy/Qsor.Deploy.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Qsor.Deploy/tools/rcedit-x64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mempler/Qsor/c1124f16faca2d46c642be57651690558be1e599/Qsor.Deploy/tools/rcedit-x64.exe -------------------------------------------------------------------------------- /Qsor.Desktop/Program.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework; 2 | 3 | namespace Qsor.Desktop 4 | { 5 | internal static class Program 6 | { 7 | public static void Main(params string[] args) 8 | { 9 | using var host = Host.GetSuitableDesktopHost("Qsor"); 10 | using var game = new QsorGameDesktop(args); 11 | 12 | host.Run(game); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Qsor.Desktop/Qsor.Desktop.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Qsor 5 | WinExe 6 | net8.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Qsor.Desktop/QsorGameDesktop.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using osu.Framework; 4 | using osu.Framework.Development; 5 | using Qsor.Desktop.Updater; 6 | using Qsor.Game; 7 | using Qsor.Game.Updater; 8 | 9 | namespace Qsor.Desktop; 10 | 11 | public partial class QsorGameDesktop : QsorGame 12 | { 13 | public QsorGameDesktop(string[] args = null) 14 | : base(args) 15 | { 16 | } 17 | 18 | protected override UpdateManager CreateUpdater() 19 | { 20 | if (DebugUtils.IsDebugBuild) 21 | return new DummyUpdater(); 22 | 23 | switch (RuntimeInfo.OS) 24 | { 25 | case RuntimeInfo.Platform.Windows: 26 | Debug.Assert(OperatingSystem.IsWindows()); // To silence the "only supported on Windows" warning 27 | 28 | return new SquirrelUpdateManager(); 29 | 30 | default: 31 | return new DummyUpdater(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Qsor.Desktop/Updater/SquirrelUpdateManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Reflection; 5 | using System.Runtime.Versioning; 6 | using System.Threading; 7 | using Newtonsoft.Json; 8 | using osu.Framework.Allocation; 9 | using osu.Framework.Platform; 10 | using Qsor.Game.Updater; 11 | using Squirrel; 12 | using Squirrel.Sources; 13 | using UpdateManager = Qsor.Game.Updater.UpdateManager; 14 | 15 | namespace Qsor.Desktop.Updater 16 | { 17 | [Cached] 18 | [SupportedOSPlatform("windows")] 19 | public partial class SquirrelUpdateManager : UpdateManager, IDisposable 20 | { 21 | [Resolved] 22 | private GameHost Host { get; set; } 23 | 24 | private Squirrel.UpdateManager _updateManager; 25 | 26 | 27 | [BackgroundDependencyLoader] 28 | private void Load() 29 | { 30 | const string GITHUB_ACCESS_TOKEN = null; // TODO: fill in your GitHub access token here 31 | 32 | _updateManager = new Squirrel.UpdateManager(new GithubSource("https://github.com/mempler/Qsor/", GITHUB_ACCESS_TOKEN, true), "qsor"); 33 | } 34 | 35 | public override async void CheckAvailable() 36 | { 37 | while (_updateManager == null) 38 | Thread.Sleep(1); 39 | 40 | var updateInfo = await _updateManager.CheckForUpdate(); 41 | 42 | if (updateInfo.ReleasesToApply.Count > 0) 43 | BindableStatus.Value = UpdaterStatus.Pending; 44 | } 45 | 46 | private bool _hasStarted; 47 | public override async void UpdateGame() 48 | { 49 | if (BindableStatus.Value == UpdaterStatus.Ready) 50 | { 51 | var entry = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location) ?? throw new NullReferenceException(); 52 | var path = Path.Join(entry, "../"); 53 | 54 | Console.WriteLine(path); 55 | 56 | Process.Start(Path.Join(path, "./Qsor.exe"), "--updated"); 57 | 58 | Host.Exit(); 59 | } 60 | 61 | if (_hasStarted) 62 | return; 63 | 64 | BindableStatus.Value = UpdaterStatus.Downloading; 65 | 66 | _hasStarted = true; 67 | 68 | await _updateManager.UpdateApp(progress => BindableProgress.Value = progress); 69 | 70 | BindableStatus.Value = UpdaterStatus.Ready; 71 | } 72 | 73 | protected override void Dispose(bool isDisposing) 74 | { 75 | base.Dispose(isDisposing); 76 | _updateManager?.Dispose(); 77 | } 78 | 79 | public class GitHubRelease 80 | { 81 | [JsonProperty("tag_name")] 82 | public string TagName { get; set; } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Qsor.Desktop/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mempler/Qsor/c1124f16faca2d46c642be57651690558be1e599/Qsor.Desktop/icon.ico -------------------------------------------------------------------------------- /Qsor.Desktop/qsor.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Qsor 5 | 0.0.0 6 | Qsor 7 | Mempler 8 | Robin A. Plate 9 | https://akatsuki.pw 10 | Copyright (c) 2024 Robin A. Plate 11 | An osu! like rythm game based ontop of o!F 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Qsor.Game/Beatmaps/Beatmap.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using osu.Framework.Platform; 5 | using osuTK.Graphics; 6 | using Component = osu.Framework.Graphics.Component; 7 | 8 | namespace Qsor.Game.Beatmaps 9 | { 10 | public partial class Beatmap : Component 11 | { 12 | public class GeneralSection 13 | { 14 | public string AudioFilename; 15 | public int AudioLeadIn; 16 | } 17 | 18 | public class DifficultySection 19 | { 20 | public double CircleSize = 5; 21 | public double ApproachRate = 5; 22 | public double OverallDifficulty = 5; 23 | 24 | // Sliders 25 | public double SliderMultiplier; 26 | public double TickRate; 27 | } 28 | 29 | public int BeatmapVersion = 0; 30 | 31 | public readonly GeneralSection General = new(); 32 | public readonly DifficultySection Difficulty = new(); 33 | public readonly List Colors = new(); 34 | public List TimingPoints = new(); 35 | 36 | public string BackgroundFilename; 37 | 38 | // TODO: maybe move this to WorkingBeatmap ? 39 | protected Storage BeatmapStorage { get; private set; } 40 | 41 | public TimingPoint GetTimingPointAt(double time) => TimingPoints.FirstOrDefault(t => t.Offset >= time); 42 | 43 | public static T ReadBeatmap(Storage storage, string fileName) 44 | where T : Beatmap, new() 45 | { 46 | if (!storage.Exists(fileName)) 47 | throw new FileNotFoundException("Beatmap file hasn't been found!", fileName); 48 | 49 | var reader = new StreamReader(storage.GetStream(fileName)); 50 | var bmContent = reader.ReadToEnd(); 51 | 52 | var parser = new BeatmapParser(); 53 | 54 | var bm = parser.ConstructFromString(bmContent); 55 | bm.BeatmapStorage = storage; 56 | 57 | return bm; 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /Qsor.Game/Beatmaps/BeatmapContainer.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Audio; 3 | using osu.Framework.Bindables; 4 | using osu.Framework.Graphics; 5 | using osu.Framework.Graphics.Containers; 6 | using osu.Framework.Graphics.Textures; 7 | using Qsor.Game.Graphics.Containers; 8 | 9 | namespace Qsor.Game.Beatmaps 10 | { 11 | public partial class BeatmapContainer : Container 12 | { 13 | private BackgroundImageContainer _background; 14 | public Bindable WorkingBeatmap { get; } = new(); 15 | 16 | [Resolved] 17 | private AudioManager Audio { get; set; } 18 | 19 | public BeatmapContainer(Bindable beatmap) 20 | { 21 | WorkingBeatmap.BindTo(beatmap); 22 | } 23 | 24 | [BackgroundDependencyLoader] 25 | private void Load(TextureStore store) 26 | { 27 | RelativeSizeAxes = Axes.Both; 28 | Anchor = Anchor.Centre; 29 | Origin = Anchor.Centre; 30 | FillMode = FillMode.Fill; 31 | 32 | LoadComponent(WorkingBeatmap.Value); 33 | 34 | AddInternal(_background = new BackgroundImageContainer 35 | { 36 | RelativeSizeAxes = Axes.Both, 37 | Anchor = Anchor.Centre, 38 | Origin = Anchor.Centre, 39 | FillMode = FillMode.Fill, 40 | }); 41 | 42 | _background.SetTexture(WorkingBeatmap.Value.Background); 43 | Audio.AddItem(WorkingBeatmap.Value.Track); 44 | } 45 | 46 | public void PlayBeatmap() 47 | { 48 | WorkingBeatmap.Value.Track.Start(); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /Qsor.Game/Beatmaps/BeatmapManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using ICSharpCode.SharpZipLib.Zip; 6 | using osu.Framework.Allocation; 7 | using osu.Framework.Audio; 8 | using osu.Framework.Bindables; 9 | using osu.Framework.Graphics.Textures; 10 | using osu.Framework.Platform; 11 | using Qsor.Game.Configuration; 12 | using Qsor.Game.Database; 13 | using Qsor.Game.Database.Models; 14 | using Qsor.Game.Graphics.Containers; 15 | using Qsor.Game.Online; 16 | 17 | namespace Qsor.Game.Beatmaps 18 | { 19 | public partial class BeatmapManager : IDependencyInjectionCandidate 20 | { 21 | public Bindable WorkingBeatmap { get; } = new(); 22 | 23 | [Resolved] 24 | private BeatmapMirrorAccess MirrorAccess { get; set; } 25 | 26 | [Resolved] 27 | private Storage Storage { get; set; } 28 | 29 | [Resolved] 30 | private QsorDbContextFactory QsorDbContextFactory { get; set; } 31 | 32 | [Resolved] 33 | private AudioManager AudioManager { get; set; } 34 | 35 | [Resolved] 36 | private QsorConfigManager ConfigManager { get; set; } 37 | 38 | [Resolved] 39 | private QsorDbContextFactory DbContextFactory { get; set; } 40 | 41 | public BackgroundImageContainer Background; 42 | 43 | public BeatmapManager() 44 | { 45 | WorkingBeatmap.ValueChanged += e => 46 | { 47 | e.OldValue?.Track.Stop(); 48 | e.OldValue?.Dispose(); 49 | }; 50 | } 51 | 52 | [BackgroundDependencyLoader] 53 | private void Load(TextureStore store) 54 | { 55 | // TODO: Remove 56 | var setId = ConfigManager.Get(QsorSetting.BeatmapSetId); 57 | if (!Storage.ExistsDirectory($"./Songs/{setId}")) 58 | { 59 | MirrorAccess.DownloadBeatmap(setId); 60 | 61 | ImportStalled(); 62 | } 63 | } 64 | 65 | /// 66 | /// Import all .osz files inside %QSOR_DIR%/Songs/ 67 | /// 68 | public void ImportStalled() 69 | { 70 | foreach (var osz in Storage.GetFiles("./Songs/", "*.osz")) 71 | { 72 | using (var oszStream = Storage.GetStream(osz, FileAccess.Read, FileMode.Open)) 73 | { 74 | ImportZip(oszStream); 75 | } 76 | 77 | Storage.Delete(osz); // Cleanup 78 | } 79 | } 80 | 81 | public void ImportZip(Stream beatmapFile) 82 | { 83 | using var db = QsorDbContextFactory.GetForWrite(); 84 | using var s = new ZipInputStream(beatmapFile); 85 | 86 | ZipEntry theEntry; 87 | while ((theEntry = s.GetNextEntry()) != null) 88 | { 89 | var directoryStorage = Storage.GetStorageForDirectory($"./Songs/{ConfigManager.Get(QsorSetting.BeatmapSetId)}/" + Path.GetDirectoryName(theEntry.Name)); 90 | var fileName = Path.GetFileName(theEntry.Name); 91 | 92 | if (fileName == string.Empty) continue; 93 | 94 | { 95 | using var streamWriter = directoryStorage.GetStream(theEntry.Name, FileAccess.Write); 96 | var data = new byte[2048]; 97 | 98 | while (true) 99 | { 100 | var size = s.Read(data, 0, data.Length); 101 | if (size > 0) 102 | streamWriter.Write(data, 0, size); 103 | else 104 | break; 105 | } 106 | } 107 | 108 | if (!fileName?.EndsWith(".osu") ?? false) 109 | continue; 110 | 111 | var beatmapFilePath = directoryStorage.GetFullPath(theEntry.Name); 112 | var beatmap = Beatmap.ReadBeatmap(directoryStorage, beatmapFilePath); 113 | 114 | db.Context.Beatmaps.Add(new BeatmapModel 115 | { 116 | File = fileName, 117 | Audio = beatmap.General.AudioFilename, 118 | Path = directoryStorage.GetFullPath(string.Empty), 119 | Thumbnail = beatmap.BackgroundFilename 120 | }); 121 | } 122 | } 123 | 124 | public BeatmapContainer LoadBeatmap(Storage storage, string fileName) 125 | { 126 | WorkingBeatmap.Value = Beatmap.ReadBeatmap(storage, fileName); 127 | 128 | return new BeatmapContainer(WorkingBeatmap); 129 | } 130 | 131 | private readonly List _alreadyRandomized = new(); 132 | 133 | public BeatmapContainer NextRandomMap() 134 | { 135 | WorkingBeatmap.Value?.Track.Stop(); 136 | 137 | var ctx = DbContextFactory.Get(); 138 | while (true) 139 | { 140 | var beatmapModel = ctx.Beatmaps.Where(s => !_alreadyRandomized.Contains(s.Id)) 141 | .ToList() 142 | .OrderBy(_ => Guid.NewGuid()) 143 | .FirstOrDefault(); 144 | 145 | // Never repeating beatmaps 146 | if (beatmapModel == null) 147 | { 148 | // We do not have a single beatmap we could use 149 | if (_alreadyRandomized.Count <= 0) 150 | { 151 | return null; 152 | } 153 | 154 | _alreadyRandomized.Clear(); 155 | continue; 156 | } 157 | 158 | var beatmapStorage = Storage.GetStorageForDirectory(beatmapModel?.Path); 159 | _alreadyRandomized.Add(beatmapModel.Id); 160 | return LoadBeatmap(beatmapStorage, beatmapModel.File); 161 | } 162 | } 163 | 164 | public BeatmapContainer PreviousRandomMap() 165 | { 166 | var ctx = DbContextFactory.Get(); 167 | 168 | while (!ctx.Beatmaps.Any(s => s.Id == _alreadyRandomized.LastOrDefault())) 169 | { 170 | if (_alreadyRandomized.Count <= 0) 171 | { 172 | // We don't have any beatmaps to randomize take the next one 173 | return NextRandomMap(); 174 | } 175 | 176 | // Beatmap deleted, lets try again 177 | _alreadyRandomized.RemoveAt(_alreadyRandomized.Count - 1); 178 | } 179 | 180 | _alreadyRandomized.RemoveAt(_alreadyRandomized.Count - 1); // Pop back 181 | 182 | var beatmapModel = ctx.Beatmaps 183 | .FirstOrDefault(s => s.Id == _alreadyRandomized.LastOrDefault()); 184 | 185 | if (beatmapModel != null) 186 | { 187 | var beatmapStorage = Storage.GetStorageForDirectory(beatmapModel.Path); 188 | return LoadBeatmap(beatmapStorage, beatmapModel.File); 189 | } 190 | 191 | // Couldn't find the last map, get the next map instead! 192 | return NextRandomMap(); 193 | } 194 | } 195 | } -------------------------------------------------------------------------------- /Qsor.Game/Beatmaps/TimingPoint.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | 3 | namespace Qsor.Game.Beatmaps 4 | { 5 | public class TimingPoint 6 | { 7 | public double Offset; 8 | public double MsPerBeat; 9 | public int Meter; 10 | public int SampleSet; 11 | public int SampleIndex; 12 | public int Volume; 13 | public bool Inherited; 14 | public bool KiaiMode; 15 | 16 | [CanBeNull] public TimingPoint Parent = null; 17 | } 18 | } -------------------------------------------------------------------------------- /Qsor.Game/Beatmaps/WorkingBeatmap.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Audio; 3 | using osu.Framework.Audio.Track; 4 | using osu.Framework.Graphics.Rendering; 5 | using osu.Framework.Graphics.Textures; 6 | using osu.Framework.IO.Stores; 7 | 8 | namespace Qsor.Game.Beatmaps 9 | { 10 | public partial class WorkingBeatmap : Beatmap 11 | { 12 | private StorageBackedResourceStore _beatmapStore; 13 | private ITrackStore _trackStore; 14 | 15 | public Track Track; 16 | public Texture Background; 17 | 18 | [BackgroundDependencyLoader] 19 | private void Load(IRenderer renderer, AudioManager audio, QsorBaseGame game) 20 | { 21 | _trackStore = audio.GetTrackStore(_beatmapStore = new StorageBackedResourceStore(BeatmapStorage)); 22 | 23 | // TODO: maybe don't do that. 24 | Track = _trackStore.Get(General.AudioFilename); 25 | Background = Texture.FromStream(renderer, BeatmapStorage.GetStream(BackgroundFilename)); 26 | } 27 | 28 | public void Play() => Track.Start(); 29 | 30 | protected override void Dispose(bool isDisposing) 31 | { 32 | if (isDisposing) 33 | { 34 | base.Dispose(true); 35 | return; 36 | } 37 | 38 | _trackStore?.Dispose(); 39 | _beatmapStore?.Dispose(); 40 | 41 | base.Dispose(false); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /Qsor.Game/Configuration/QsorConfiguration.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Configuration; 2 | using osu.Framework.Platform; 3 | 4 | namespace Qsor.Game.Configuration 5 | { 6 | public class QsorConfigManager : IniConfigManager 7 | { 8 | protected override void InitialiseDefaults() 9 | { 10 | SetDefault(QsorSetting.BeatmapSetId, 756794); 11 | SetDefault(QsorSetting.BeatmapFile, "TheFatRat - Mayday (feat. Laura Brehm) (Voltaeyx) [[2B] Calling Out Mayday].osu"); 12 | } 13 | 14 | public QsorConfigManager(Storage storage) : 15 | base(storage) 16 | { 17 | } 18 | } 19 | 20 | public enum QsorSetting 21 | { 22 | BeatmapFile, 23 | BeatmapSetId, 24 | } 25 | } -------------------------------------------------------------------------------- /Qsor.Game/Database/DatabaseWriteUsage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Qsor.Game.Database 5 | { 6 | // Modified version of https://github.com/ppy/osu/blob/master/osu.Game/Database/DatabaseWriteUsage.cs under MIT License! 7 | public class DatabaseWriteUsage : IDisposable 8 | { 9 | public readonly QsorDbContext Context; 10 | private readonly Action _usageCompleted; 11 | public readonly List Errors = new(); 12 | 13 | private bool _isDisposed; 14 | 15 | /// 16 | /// Whether this write usage will commit a transaction on completion. 17 | /// If false, there is a parent usage responsible for transaction commit. 18 | /// 19 | public bool IsTransactionLeader = false; 20 | 21 | public DatabaseWriteUsage(QsorDbContext context, Action onCompleted) 22 | { 23 | Context = context; 24 | _usageCompleted = onCompleted; 25 | } 26 | 27 | public bool PerformedWrite { get; private set; } 28 | 29 | public void Dispose() 30 | { 31 | Dispose(true); 32 | GC.SuppressFinalize(this); 33 | } 34 | 35 | protected void Dispose(bool disposing) 36 | { 37 | if (_isDisposed) 38 | return; 39 | 40 | _isDisposed = true; 41 | 42 | try 43 | { 44 | PerformedWrite |= Context.SaveChanges() > 0; 45 | } catch (Exception e) 46 | { 47 | Errors.Add(e); 48 | throw; 49 | } 50 | finally 51 | { 52 | _usageCompleted?.Invoke(this); 53 | } 54 | } 55 | 56 | ~DatabaseWriteUsage() 57 | { 58 | Dispose(false); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /Qsor.Game/Database/Migrations/20200331222405_BeatmapModel.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | 7 | namespace Qsor.Game.Database.Migrations 8 | { 9 | [DbContext(typeof(QsorDbContext))] 10 | [Migration("20200331222405_BeatmapModel")] 11 | partial class BeatmapModel 12 | { 13 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 14 | { 15 | #pragma warning disable 612, 618 16 | modelBuilder 17 | .HasAnnotation("ProductVersion", "3.1.3"); 18 | 19 | modelBuilder.Entity("Qsor.Game.Database.Models.BeatmapModel", b => 20 | { 21 | b.Property("Id") 22 | .ValueGeneratedOnAdd() 23 | .HasColumnType("INTEGER"); 24 | 25 | b.Property("Audio") 26 | .HasColumnType("TEXT"); 27 | 28 | b.Property("File") 29 | .HasColumnType("TEXT"); 30 | 31 | b.Property("Path") 32 | .HasColumnType("TEXT"); 33 | 34 | b.Property("Thumbnail") 35 | .HasColumnType("TEXT"); 36 | 37 | b.HasKey("Id"); 38 | 39 | b.ToTable("beatmaps"); 40 | }); 41 | #pragma warning restore 612, 618 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Qsor.Game/Database/Migrations/20200331222405_BeatmapModel.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace Qsor.Game.Database.Migrations 4 | { 5 | public partial class BeatmapModel : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.CreateTable( 10 | name: "beatmaps", 11 | columns: table => new 12 | { 13 | Id = table.Column(nullable: false) 14 | .Annotation("Sqlite:Autoincrement", true), 15 | File = table.Column(nullable: true), 16 | Path = table.Column(nullable: true), 17 | Audio = table.Column(nullable: true), 18 | Thumbnail = table.Column(nullable: true) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_beatmaps", x => x.Id); 23 | }); 24 | } 25 | 26 | protected override void Down(MigrationBuilder migrationBuilder) 27 | { 28 | migrationBuilder.DropTable( 29 | name: "beatmaps"); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Qsor.Game/Database/Migrations/QsorDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | 6 | namespace Qsor.Game.Database.Migrations 7 | { 8 | [DbContext(typeof(QsorDbContext))] 9 | partial class QsorDbContextModelSnapshot : ModelSnapshot 10 | { 11 | protected override void BuildModel(ModelBuilder modelBuilder) 12 | { 13 | #pragma warning disable 612, 618 14 | modelBuilder 15 | .HasAnnotation("ProductVersion", "3.1.3"); 16 | 17 | modelBuilder.Entity("Qsor.Game.Database.Models.BeatmapModel", b => 18 | { 19 | b.Property("Id") 20 | .ValueGeneratedOnAdd() 21 | .HasColumnType("INTEGER"); 22 | 23 | b.Property("Audio") 24 | .HasColumnType("TEXT"); 25 | 26 | b.Property("File") 27 | .HasColumnType("TEXT"); 28 | 29 | b.Property("Path") 30 | .HasColumnType("TEXT"); 31 | 32 | b.Property("Thumbnail") 33 | .HasColumnType("TEXT"); 34 | 35 | b.HasKey("Id"); 36 | 37 | b.ToTable("beatmaps"); 38 | }); 39 | #pragma warning restore 612, 618 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Qsor.Game/Database/Models/BeatmapModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | 3 | namespace Qsor.Game.Database.Models 4 | { 5 | [Table("beatmaps")] 6 | public class BeatmapModel 7 | { 8 | public int Id { get; set; } 9 | public string File { get; set; } // this.osu 10 | public string Path { get; set; } // /Songs/.../this.osu 11 | 12 | public string Audio { get; set; } 13 | public string Thumbnail { get; set; } 14 | 15 | // TODO: Difficulty and other information pre-cached. 16 | } 17 | } -------------------------------------------------------------------------------- /Qsor.Game/Database/QsorDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using osu.Framework.Logging; 3 | using Qsor.Game.Database.Models; 4 | 5 | namespace Qsor.Game.Database 6 | { 7 | public sealed class QsorDbContext : DbContext 8 | { 9 | private readonly string _connectionString; 10 | 11 | public QsorDbContext() 12 | : this("DataSource=:memory:") 13 | { 14 | } 15 | 16 | public QsorDbContext(string connectionString) 17 | { 18 | _connectionString = connectionString; 19 | 20 | var connection = Database.GetDbConnection(); 21 | 22 | try 23 | { 24 | connection.Open(); 25 | 26 | using var cmd = connection.CreateCommand(); 27 | 28 | cmd.CommandText = "PRAGMA journal_mode=WAL;"; 29 | cmd.ExecuteNonQuery(); 30 | } 31 | catch 32 | { 33 | connection.Close(); 34 | throw; 35 | } 36 | } 37 | 38 | public DbSet Beatmaps { get; set; } 39 | 40 | public void Migrate() 41 | { 42 | Logger.LogPrint("Migrating Database"); 43 | Database.Migrate(); 44 | } 45 | 46 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 47 | { 48 | optionsBuilder.UseSqlite(_connectionString); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /Qsor.Game/Database/QsorDbContextFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading; 3 | using Microsoft.EntityFrameworkCore.Storage; 4 | using osu.Framework.Platform; 5 | 6 | namespace Qsor.Game.Database 7 | { 8 | // Modified version of https://github.com/ppy/osu/blob/master/osu.Game/Database/DatabaseContextFactory.cs under MIT License! 9 | public sealed class QsorDbContextFactory 10 | { 11 | private readonly Storage _storage; 12 | private readonly object _writeLock = new(); 13 | private bool _currentWriteDidError; 14 | 15 | private bool _currentWriteDidWrite; 16 | 17 | private IDbContextTransaction _currentWriteTransaction; 18 | 19 | private int _currentWriteUsages; 20 | private ThreadLocal _threadContexts; 21 | 22 | public QsorDbContextFactory(Storage storage) 23 | { 24 | _storage = storage; 25 | RecycleThreadContexts(); 26 | } 27 | 28 | /// 29 | /// Get a context for the current thread for read-only usage. 30 | /// If a is in progress, the existing write-safe context will be returned. 31 | /// 32 | public QsorDbContext Get() => _threadContexts.Value; 33 | 34 | /// 35 | /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying 36 | /// context). 37 | /// This method may block if a write is already active on a different thread. 38 | /// 39 | /// Whether to start a transaction for this write. 40 | /// A usage containing a usable context. 41 | public DatabaseWriteUsage GetForWrite(bool withTransaction = true) 42 | { 43 | Monitor.Enter(_writeLock); 44 | QsorDbContext context; 45 | 46 | try 47 | { 48 | if (_currentWriteTransaction == null && withTransaction) 49 | { 50 | // this mitigates the fact that changes on tracked entities will not be rolled back with the transaction by ensuring write operations are always executed in isolated contexts. 51 | // if this results in sub-optimal efficiency, we may need to look into removing Database-level transactions in favour of running SaveChanges where we currently commit the transaction. 52 | if (_threadContexts.IsValueCreated) 53 | RecycleThreadContexts(); 54 | 55 | context = _threadContexts.Value; 56 | _currentWriteTransaction = context.Database.BeginTransaction(); 57 | } 58 | else 59 | { 60 | // we want to try-catch the retrieval of the context because it could throw an error (in CreateContext). 61 | context = _threadContexts.Value; 62 | } 63 | } catch 64 | { 65 | // retrieval of a context could trigger a fatal error. 66 | Monitor.Exit(_writeLock); 67 | throw; 68 | } 69 | 70 | Interlocked.Increment(ref _currentWriteUsages); 71 | 72 | return new DatabaseWriteUsage(context, UsageCompleted) 73 | { 74 | IsTransactionLeader = _currentWriteTransaction != null && _currentWriteUsages == 1 75 | }; 76 | } 77 | 78 | private void UsageCompleted(DatabaseWriteUsage usage) 79 | { 80 | var usages = Interlocked.Decrement(ref _currentWriteUsages); 81 | 82 | try 83 | { 84 | _currentWriteDidWrite |= usage.PerformedWrite; 85 | _currentWriteDidError |= usage.Errors.Any(); 86 | 87 | if (usages == 0) 88 | { 89 | if (_currentWriteDidError) 90 | _currentWriteTransaction?.Rollback(); 91 | else 92 | _currentWriteTransaction?.Commit(); 93 | 94 | if (_currentWriteDidWrite || _currentWriteDidError) 95 | { 96 | // explicitly dispose to ensure any outstanding flushes happen as soon as possible (and underlying resources are purged). 97 | usage.Context.Dispose(); 98 | 99 | // once all writes are complete, we want to refresh thread-specific contexts to make sure they don't have stale local caches. 100 | RecycleThreadContexts(); 101 | } 102 | 103 | _currentWriteTransaction = null; 104 | _currentWriteDidWrite = false; 105 | _currentWriteDidError = false; 106 | } 107 | } 108 | finally 109 | { 110 | Monitor.Exit(_writeLock); 111 | } 112 | } 113 | 114 | private void RecycleThreadContexts() 115 | { 116 | // Contexts for other threads are not disposed as they may be in use elsewhere. Instead, fresh contexts are exposed 117 | // for other threads to use, and we rely on the finalizer inside SoraDbContext to handle their previous contexts 118 | _threadContexts?.Value.Dispose(); 119 | _threadContexts = new ThreadLocal(CreateContext, true); 120 | } 121 | 122 | private QsorDbContext CreateContext() 123 | => new(_storage.GetDatabaseConnectionString("qsor")) { 124 | Database = { 125 | AutoTransactionBehavior = Microsoft.EntityFrameworkCore.AutoTransactionBehavior.Always 126 | } 127 | }; 128 | 129 | public void ResetDatabase() 130 | { 131 | lock (_writeLock) 132 | { 133 | RecycleThreadContexts(); 134 | try 135 | { 136 | _storage.DeleteDatabase("qsor"); 137 | } 138 | catch 139 | { 140 | // for now we are not sure why file handles are kept open by EF, but this is generally only used in testing 141 | } 142 | } 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /Qsor.Game/Database/StorageExtension.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Platform; 2 | 3 | namespace Qsor.Game.Database 4 | { 5 | // Bring back storage stuff which was deleted from o!F some time ago 6 | internal static class StorageExtension 7 | { 8 | public static string GetDatabaseConnectionString(this Storage storage, string name) 9 | => string.Concat("Data Source=", storage.GetFullPath($@"{name}.db", true)); 10 | 11 | public static void DeleteDatabase(this Storage storage, string name) 12 | => storage.Delete($@"{name}.db"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Qsor.Game/Graphics/ClickableSpriteIcon.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Cursor; 2 | using osu.Framework.Graphics.Sprites; 3 | using osu.Framework.Input.Events; 4 | using osu.Framework.Localisation; 5 | 6 | namespace Qsor.Game.Graphics 7 | { 8 | public partial class ClickableSpriteIcon : SpriteIcon, IHasTooltip 9 | { 10 | public LocalisableString TooltipText { get; set; } 11 | 12 | public delegate bool ClickDelegate(ClickEvent e); 13 | public ClickDelegate ClickEvent; 14 | 15 | protected override bool OnClick(ClickEvent e) 16 | { 17 | return ClickEvent?.Invoke(e) ?? false; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/Containers/BackgroundImageContainer.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Containers; 4 | using osu.Framework.Graphics.Sprites; 5 | using osu.Framework.Graphics.Textures; 6 | using osuTK; 7 | using osuTK.Graphics; 8 | 9 | namespace Qsor.Game.Graphics.Containers 10 | { 11 | public partial class BackgroundImageContainer : BufferedContainer 12 | { 13 | private Sprite _backgroundImage; 14 | 15 | [BackgroundDependencyLoader] 16 | private void Load() 17 | { 18 | RelativeSizeAxes = Axes.Both; 19 | 20 | Child = _backgroundImage = new Sprite 21 | { 22 | RelativeSizeAxes = Axes.Both, 23 | Anchor = Anchor.Centre, 24 | Origin = Anchor.Centre, 25 | FillMode = FillMode.Fill 26 | }; 27 | 28 | BackgroundColour = Color4.Black; 29 | Colour = new Color4(1,1,1, .5f); 30 | BlurSigma = new Vector2(5f); 31 | } 32 | 33 | public void SetTexture(Texture tex) 34 | { 35 | _backgroundImage.Texture = tex; 36 | 37 | ForceRedraw(); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/Containers/BeatSyncedContainer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. 2 | // See the LICENCE file in the repository root for full licence text. 3 | 4 | using osu.Framework.Allocation; 5 | using osu.Framework.Audio.Track; 6 | using osu.Framework.Bindables; 7 | using osu.Framework.Graphics.Containers; 8 | using Qsor.Game.Beatmaps; 9 | 10 | namespace Qsor.Game.Graphics.Containers 11 | { 12 | public partial class BeatSyncedContainer : Container 13 | { 14 | protected readonly IBindable Beatmap = new Bindable(); 15 | 16 | private int _lastBeat; 17 | private TimingPoint _lastTimingPoint; 18 | 19 | /// 20 | /// The amount of time before a beat we should fire . 21 | /// This allows for adding easing to animations that may be synchronised to the beat. 22 | /// 23 | protected double EarlyActivationMilliseconds; 24 | 25 | /// 26 | /// The time in milliseconds until the next beat. 27 | /// 28 | public double TimeUntilNextBeat { get; private set; } 29 | 30 | /// 31 | /// The time in milliseconds since the last beat 32 | /// 33 | public double TimeSinceLastBeat { get; private set; } 34 | 35 | /// 36 | /// How many beats per beatlength to trigger. Defaults to 1. 37 | /// 38 | public int Divisor { get; set; } = 1; 39 | 40 | /// 41 | /// An optional minimum beat length. Any beat length below this will be multiplied by two until valid. 42 | /// 43 | public double MinimumBeatLength { get; set; } 44 | 45 | /// 46 | /// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing. 47 | /// 48 | private const double DefaultBeatLength = 60000.0 / 60.0; 49 | 50 | private TimingPoint _defaultTiming; 51 | //private EffectControlPoint defaultEffect; 52 | private ChannelAmplitudes _defaultAmplitudes; 53 | 54 | protected bool IsBeatSyncedWithTrack { get; private set; } 55 | protected bool IsTrackPaused => Beatmap?.Value?.Track?.IsRunning ?? true; 56 | 57 | private TimingPoint _lastValidTimingPoint; 58 | 59 | protected override void Update() 60 | { 61 | Track track = null; 62 | Beatmap beatmap = null; 63 | 64 | var currentTrackTime = 0d; 65 | var timingPoint = new TimingPoint(); 66 | //EffectControlPoint effectPoint = null; 67 | 68 | if (Beatmap.Value?.Track?.IsLoaded == true) 69 | { 70 | track = Beatmap.Value.Track; 71 | beatmap = Beatmap.Value; 72 | } 73 | 74 | if (track != null && beatmap != null && track.IsRunning && track.Length > 0) 75 | { 76 | currentTrackTime = track.CurrentTime + EarlyActivationMilliseconds; 77 | 78 | timingPoint = beatmap.GetTimingPointAt(currentTrackTime); 79 | } 80 | 81 | timingPoint ??= _defaultTiming; 82 | 83 | IsBeatSyncedWithTrack = timingPoint.MsPerBeat > 0; 84 | 85 | if (!IsBeatSyncedWithTrack) 86 | { 87 | // inherit kiai mode 88 | if (_lastValidTimingPoint != null && timingPoint != null && timingPoint != _defaultTiming) 89 | _lastValidTimingPoint.KiaiMode = timingPoint.KiaiMode; 90 | 91 | currentTrackTime = Clock.CurrentTime; 92 | timingPoint = _lastValidTimingPoint ?? _defaultTiming; 93 | } 94 | else 95 | { 96 | _lastValidTimingPoint = timingPoint; 97 | } 98 | 99 | if (!track?.IsRunning ?? true) 100 | { 101 | currentTrackTime = Clock.CurrentTime; 102 | timingPoint = _defaultTiming; 103 | } 104 | 105 | var beatLength = timingPoint.MsPerBeat / Divisor; 106 | 107 | while (beatLength < MinimumBeatLength) 108 | beatLength *= 2; 109 | 110 | var beatIndex = (int)((currentTrackTime - timingPoint.Offset) / beatLength); 111 | 112 | // The beats before the start of the first control point are off by 1, this should do the trick 113 | if (currentTrackTime < timingPoint.Offset) 114 | beatIndex--; 115 | 116 | TimeUntilNextBeat = (timingPoint.Offset - currentTrackTime) % beatLength; 117 | if (double.IsNaN(TimeUntilNextBeat)) 118 | TimeUntilNextBeat = 0; 119 | 120 | if (TimeUntilNextBeat < 0) 121 | TimeUntilNextBeat += beatLength; 122 | 123 | TimeSinceLastBeat = beatLength - TimeUntilNextBeat; 124 | 125 | if (timingPoint.Equals(_lastTimingPoint) && beatIndex == _lastBeat) 126 | return; 127 | 128 | using (BeginDelayedSequence(-TimeSinceLastBeat, true)) 129 | OnNewBeat(beatIndex, timingPoint, track?.CurrentAmplitudes ?? _defaultAmplitudes); 130 | 131 | _lastBeat = beatIndex; 132 | _lastTimingPoint = timingPoint; 133 | } 134 | 135 | [BackgroundDependencyLoader] 136 | private void Load(BeatmapManager beatmapManager) 137 | { 138 | Beatmap.BindTo(beatmapManager.WorkingBeatmap); 139 | 140 | _defaultTiming = new TimingPoint 141 | { 142 | MsPerBeat = DefaultBeatLength, 143 | }; 144 | 145 | _defaultAmplitudes = new ChannelAmplitudes(0, 0, new float[256]); 146 | } 147 | 148 | protected virtual void OnNewBeat(int beatIndex, TimingPoint timingPoint, ChannelAmplitudes amplitudes) 149 | { 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/Containers/ParallaxContainer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. 2 | // See the LICENCE file in the repository root for full licence text. 3 | 4 | using System; 5 | using osu.Framework.Allocation; 6 | using osu.Framework.Graphics; 7 | using osu.Framework.Graphics.Containers; 8 | using osu.Framework.Input; 9 | using osu.Framework.Utils; 10 | using osuTK; 11 | 12 | namespace Qsor.Game.Graphics.Containers 13 | { 14 | public partial class ParallaxContainer : Container, IRequireHighFrequencyMousePosition 15 | { 16 | public const float DefaultParallaxAmount = 0.02f; 17 | 18 | /// 19 | /// The amount of parallax movement. Negative values will reverse the direction of parallax relative to user input. 20 | /// 21 | public float ParallaxAmount = DefaultParallaxAmount; 22 | public ParallaxContainer() 23 | { 24 | RelativeSizeAxes = Axes.Both; 25 | 26 | AddInternal(_content = new Container 27 | { 28 | RelativeSizeAxes = Axes.Both, 29 | Anchor = Anchor.Centre, 30 | Origin = Anchor.Centre 31 | }); 32 | } 33 | 34 | public readonly Container _content; 35 | private InputManager _input; 36 | 37 | protected override Container Content => _content; 38 | 39 | [BackgroundDependencyLoader] 40 | private void Load() 41 | { 42 | _content.MoveTo(Vector2.Zero, _firstUpdate ? 0 : 1000, Easing.OutQuint); 43 | _content.Scale = new Vector2(1 + Math.Abs(ParallaxAmount)); 44 | } 45 | 46 | protected override void LoadComplete() 47 | { 48 | base.LoadComplete(); 49 | _input = GetContainingInputManager(); 50 | } 51 | 52 | private bool _firstUpdate = true; 53 | 54 | protected override void Update() 55 | { 56 | base.Update(); 57 | 58 | var offset = (_input.CurrentState.Mouse == null ? Vector2.Zero : ToLocalSpace(_input.CurrentState.Mouse.Position) - DrawSize / 2) * ParallaxAmount; 59 | 60 | const float parallaxDuration = 100; 61 | 62 | var elapsed = Math.Clamp(Clock.ElapsedFrameTime, 0, parallaxDuration); 63 | 64 | _content.Position = Interpolation.ValueAt(elapsed, _content.Position, offset, 0, parallaxDuration, Easing.OutQuint); 65 | _content.Scale = Interpolation.ValueAt(elapsed, _content.Scale, new Vector2(1 + Math.Abs(ParallaxAmount)), 0, 1000, Easing.OutQuint); 66 | 67 | _firstUpdate = false; 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/Containers/QsorTooltipContainer.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Containers; 4 | using osu.Framework.Graphics.Cursor; 5 | using osu.Framework.Graphics.Shapes; 6 | using osu.Framework.Localisation; 7 | using osuTK; 8 | using osuTK.Graphics; 9 | 10 | namespace Qsor.Game.Graphics.Containers 11 | { 12 | public partial class QsorTooltipContainer : TooltipContainer 13 | { 14 | protected override ITooltip CreateTooltip() => new QsorTooltip(); 15 | 16 | protected override double AppearDelay => 120; 17 | 18 | public QsorTooltipContainer(CursorContainer cursor) 19 | : base(cursor) 20 | { 21 | } 22 | 23 | public partial class QsorTooltip : Tooltip 24 | { 25 | private TextFlowContainer _textFlowContainer; 26 | 27 | [BackgroundDependencyLoader] 28 | private void Load() 29 | { 30 | CornerRadius = 4f; 31 | 32 | Masking = true; 33 | BorderColour = Color4.Gray; 34 | BorderThickness = 2; 35 | 36 | Scale = new Vector2(.8f); 37 | 38 | AutoSizeAxes = Axes.Both; 39 | Alpha = 0; 40 | 41 | Children = new Drawable[] 42 | { 43 | new Box 44 | { 45 | Colour = Color4.Black, 46 | RelativeSizeAxes = Axes.Both, 47 | }, 48 | _textFlowContainer = new TextFlowContainer 49 | { 50 | AutoSizeAxes = Axes.Both, 51 | MaximumSize = new Vector2(400, float.MaxValue), 52 | } 53 | }; 54 | } 55 | 56 | private string _cachedString; 57 | public override void SetContent(LocalisableString content) 58 | { 59 | var contentS = content.ToString(); 60 | if (_cachedString == contentS) 61 | return; 62 | 63 | _textFlowContainer.Text = contentS; 64 | _cachedString = contentS; 65 | } 66 | 67 | protected override void PopIn() 68 | { 69 | this.FadeIn(100, Easing.In); 70 | } 71 | 72 | protected override void PopOut() 73 | { 74 | this.FadeOut(100, Easing.In); 75 | } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/DragableProgressbar.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Cursor; 2 | using osu.Framework.Input.Events; 3 | using osu.Framework.Localisation; 4 | using Qsor.Game.Graphics.Drawables; 5 | 6 | namespace Qsor.Game.Graphics 7 | { 8 | public partial class DragableProgressbar : DrawableProgressbar, IHasTooltip 9 | { 10 | public LocalisableString TooltipText { get; set; } 11 | 12 | protected override bool OnClick(ClickEvent e) 13 | { 14 | Current.Value = e.MousePosition.X / DrawWidth; 15 | return true; 16 | } 17 | 18 | protected override bool OnDragStart(DragStartEvent e) => true; 19 | 20 | protected override void OnDrag(DragEvent e) 21 | { 22 | Current.Value = e.MousePosition.X / DrawWidth; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/Drawables/DrawableProgressbar.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Graphics.Containers; 5 | using osu.Framework.Graphics.Shapes; 6 | using osu.Framework.Graphics.UserInterface; 7 | using osuTK.Graphics; 8 | 9 | namespace Qsor.Game.Graphics.Drawables 10 | { 11 | public partial class DrawableProgressbar : Container, IHasCurrentValue 12 | { 13 | private Box _progressBox; 14 | 15 | private readonly BindableWithCurrent _current = new(); 16 | 17 | public Bindable Current 18 | { 19 | get => _current.Current; 20 | set => _current.Current = value; 21 | } 22 | 23 | public DrawableProgressbar() 24 | { 25 | Height = 6; 26 | } 27 | 28 | [BackgroundDependencyLoader] 29 | private void Load() 30 | { 31 | RelativeSizeAxes = Axes.X; 32 | 33 | Masking = true; 34 | 35 | CornerRadius = 3; 36 | CornerExponent = 2f; 37 | 38 | AddInternal(new Box 39 | { 40 | Colour = new Color4(1f, 1f, 1f, 0.5f), 41 | RelativeSizeAxes = Axes.Both 42 | }); 43 | 44 | AddInternal(_progressBox = new Box 45 | { 46 | Colour = Colour, 47 | RelativeSizeAxes = Axes.Y, 48 | }); 49 | 50 | _current.ValueChanged += e => 51 | { 52 | _progressBox.Width = (int) (DrawSize.X * e.NewValue); 53 | }; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Online/Drawables/DrawableAvatar.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using osu.Framework.Allocation; 3 | using osu.Framework.Bindables; 4 | using osu.Framework.Graphics; 5 | using osu.Framework.Graphics.Containers; 6 | using osu.Framework.Graphics.Sprites; 7 | using osu.Framework.Graphics.Textures; 8 | using osuTK; 9 | using Qsor.Game.Online; 10 | using Qsor.Game.Online.Users; 11 | 12 | namespace Qsor.Game.Graphics.UserInterface.Online.Drawables 13 | { 14 | public partial class DrawableAvatar : CompositeDrawable 15 | { 16 | private Sprite Avatar { get; } = new(); 17 | 18 | public Bindable User = new(new User{ Id = 1 }); 19 | 20 | private CancellationTokenSource _cancellationTokenSource = new(); 21 | 22 | [BackgroundDependencyLoader] 23 | private void Load(TextureStore ts) 24 | { 25 | User.ValueChanged += async e 26 | => Avatar.Texture = await ts.GetAsync(BanchoClient.AvatarUrl + "/" + (e.NewValue?.Id ?? User.Default.Id), _cancellationTokenSource.Token); 27 | 28 | Avatar.FillMode = FillMode.Fit; 29 | Avatar.RelativeSizeAxes = Axes.Both; 30 | RelativeSizeAxes = Axes.Both; 31 | 32 | Margin = new MarginPadding(10); 33 | Scale = new Vector2(.8f); 34 | 35 | AddInternal(Avatar); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Online/Drawables/DrawableLevelBar.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osuTK.Graphics; 3 | using Qsor.Game.Graphics.Drawables; 4 | using Qsor.Game.Online.Users; 5 | 6 | namespace Qsor.Game.Graphics.UserInterface.Online.Drawables 7 | { 8 | public partial class DrawableLevelBar : DrawableProgressbar 9 | { 10 | private readonly User _user; 11 | 12 | public DrawableLevelBar(User user) 13 | { 14 | _user = user; 15 | } 16 | 17 | [BackgroundDependencyLoader] 18 | private void Load() 19 | { 20 | Colour = Color4.Yellow; 21 | } 22 | 23 | protected override void LoadComplete() 24 | { 25 | Current.Value = _user.Statistics.GetProgress(); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/MusicPlayerOverlay.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Containers; 4 | using osu.Framework.Graphics.Sprites; 5 | using osuTK; 6 | using Qsor.Game.Beatmaps; 7 | using Qsor.Game.Graphics.UserInterface.Overlays.Notification; 8 | 9 | namespace Qsor.Game.Graphics.UserInterface.Overlays 10 | { 11 | public partial class MusicPlayerOverlay : CompositeDrawable 12 | { 13 | private FillFlowContainer _flow; 14 | private FillFlowContainer _icons; 15 | 16 | private DragableProgressbar _progressbar; 17 | 18 | private BeatmapManager _beatmapManager; 19 | 20 | [BackgroundDependencyLoader] 21 | private void Load(NotificationOverlay notificationOverlay, BeatmapManager beatmapManager) 22 | { 23 | _beatmapManager = beatmapManager; 24 | 25 | AutoSizeAxes = Axes.Both; 26 | Padding = new MarginPadding(8); 27 | 28 | _flow = new FillFlowContainer 29 | { 30 | AutoSizeAxes = Axes.Both, 31 | Direction = FillDirection.Vertical, 32 | Spacing = new Vector2(6), 33 | }; 34 | 35 | _icons = new FillFlowContainer 36 | { 37 | AutoSizeAxes = Axes.Both, 38 | Direction = FillDirection.Horizontal, 39 | Spacing = new Vector2(16, 0), 40 | }; 41 | 42 | _icons.Add(new ClickableSpriteIcon 43 | { 44 | Icon = FontAwesome.Solid.StepBackward, 45 | Size = new Vector2(16), 46 | 47 | TooltipText = "Previous track", 48 | 49 | ClickEvent = e => 50 | { 51 | notificationOverlay.AddBigNotification("<< Prev", 1100); 52 | 53 | beatmapManager?.PreviousRandomMap(); 54 | beatmapManager?.WorkingBeatmap?.Value?.Play(); 55 | 56 | return true; 57 | } 58 | }); 59 | 60 | _icons.Add(new ClickableSpriteIcon 61 | { 62 | Icon = FontAwesome.Solid.Play, 63 | Size = new Vector2(16), 64 | 65 | TooltipText = "Play", 66 | 67 | ClickEvent = _ => 68 | { 69 | notificationOverlay.AddBigNotification("Play", 1100); 70 | 71 | var track = beatmapManager?.WorkingBeatmap?.Value?.Track; 72 | if (track != null) 73 | { 74 | if (track.IsRunning) 75 | { 76 | track.Restart(); 77 | } 78 | else 79 | { 80 | track.Start(); 81 | } 82 | } 83 | 84 | return true; 85 | } 86 | }); 87 | 88 | _icons.Add(new ClickableSpriteIcon 89 | { 90 | Icon = FontAwesome.Solid.Pause, 91 | Size = new Vector2(16), 92 | 93 | TooltipText = "Pause", 94 | 95 | ClickEvent = e => 96 | { 97 | beatmapManager?.WorkingBeatmap?.Value?.Track?.Start(); 98 | 99 | var track = beatmapManager?.WorkingBeatmap?.Value?.Track; 100 | if (track != null) 101 | { 102 | if (track.IsRunning) 103 | { 104 | notificationOverlay.AddBigNotification("Pause", 1100); 105 | track.Stop(); 106 | } 107 | else 108 | { 109 | notificationOverlay.AddBigNotification("Unpause", 1100); 110 | track.Start(); 111 | } 112 | } 113 | else 114 | { 115 | notificationOverlay.AddBigNotification("Pause", 1100); 116 | } 117 | 118 | return true; 119 | } 120 | }); 121 | 122 | _icons.Add(new ClickableSpriteIcon 123 | { 124 | Icon = FontAwesome.Solid.Stop, 125 | Size = new Vector2(16), 126 | 127 | TooltipText = "Stop the music!", 128 | 129 | ClickEvent = e => 130 | { 131 | notificationOverlay.AddBigNotification("Stop Playing", 1100); 132 | 133 | var track = beatmapManager?.WorkingBeatmap?.Value?.Track; 134 | track?.Stop(); 135 | track?.Reset(); 136 | return true; 137 | } 138 | }); 139 | 140 | _icons.Add(new ClickableSpriteIcon 141 | { 142 | Icon = FontAwesome.Solid.StepForward, 143 | Size = new Vector2(16), 144 | 145 | TooltipText = "Next track", 146 | 147 | ClickEvent = e => 148 | { 149 | notificationOverlay.AddBigNotification(">> Next", 1100); 150 | 151 | beatmapManager?.NextRandomMap(); 152 | beatmapManager?.WorkingBeatmap?.Value?.Play(); 153 | 154 | return true; 155 | } 156 | }); 157 | 158 | _icons.Add(new ClickableSpriteIcon 159 | { 160 | Icon = FontAwesome.Solid.Info, 161 | Size = new Vector2(16), 162 | 163 | TooltipText = "View song info", 164 | 165 | ClickEvent = e => 166 | { 167 | notificationOverlay.AddBigNotification("Song info will be permanently displayed.", 1100); 168 | return true; 169 | } 170 | }); 171 | 172 | _icons.Add(new ClickableSpriteIcon 173 | { 174 | Icon = FontAwesome.Solid.Bars, 175 | Size = new Vector2(16), 176 | 177 | TooltipText = "Jump To window", 178 | 179 | ClickEvent = e => 180 | { 181 | notificationOverlay.AddBigNotification("!Unimplemented!", 1100); 182 | return true; 183 | } 184 | }); 185 | 186 | _flow.Add(_icons); 187 | 188 | _flow.Add(_progressbar = new DragableProgressbar 189 | { 190 | Position = new Vector2(0, 24), 191 | 192 | TooltipText = "Drag to seek to a specific point in the song.", 193 | }); 194 | 195 | _progressbar.Current.ValueChanged += e => 196 | { 197 | var track = _beatmapManager?.WorkingBeatmap?.Value?.Track; 198 | if (track == null) 199 | return; 200 | 201 | if (_progressbar.IsDragged) 202 | { 203 | track.Seek(e.NewValue * track.Length); 204 | } 205 | }; 206 | 207 | AddInternal(_flow); 208 | } 209 | 210 | protected override void Update() 211 | { 212 | var track = _beatmapManager?.WorkingBeatmap?.Value?.Track; 213 | if (track == null) 214 | return; 215 | 216 | _progressbar.Current.Value = track.CurrentTime / track.Length; 217 | } 218 | } 219 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Notification/Drawables/DrawableBigNotification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using osu.Framework.Allocation; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Graphics.Containers; 5 | using osu.Framework.Graphics.Shapes; 6 | using osu.Framework.Localisation; 7 | using osu.Framework.Timing; 8 | using osuTK; 9 | using osuTK.Graphics; 10 | 11 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Notification.Drawables 12 | { 13 | public partial class DrawableBigNotification : CompositeDrawable 14 | { 15 | private readonly StopwatchClock _clock; 16 | private readonly double _duration; 17 | private readonly TextFlowContainer _textFlowContainer; 18 | private Box _background; 19 | 20 | public DrawableBigNotification(LocalisableString text, int duration = -1) 21 | { 22 | _clock = new StopwatchClock(); 23 | _textFlowContainer = new TextFlowContainer 24 | { 25 | Anchor = Anchor.Centre, 26 | Origin = Anchor.Centre, 27 | 28 | Direction = FillDirection.Full, 29 | AutoSizeAxes = Axes.Both, 30 | Padding = new MarginPadding(16), 31 | TextAnchor = Anchor.Centre // Centre text 32 | }; 33 | 34 | _duration = duration == -1 ? Math.Max(3000, (double) (text.ToString().Length * 100)) : duration; 35 | _textFlowContainer.AddText(text.ToString()); 36 | } 37 | 38 | [BackgroundDependencyLoader] 39 | private void Load() 40 | { 41 | RelativeSizeAxes = Axes.X; 42 | AutoSizeAxes = Axes.Y; 43 | 44 | Scale = new Vector2(1.0f, 0.0f); 45 | 46 | AddInternal(_background = new Box 47 | { 48 | Colour = new Color4(0f,0f,0f,.5f), 49 | RelativeSizeAxes = Axes.Both 50 | }); 51 | AddInternal(_textFlowContainer); 52 | } 53 | 54 | public override void Show() 55 | { 56 | this.FadeInFromZero(100); 57 | this.ScaleTo(new Vector2(1.0f, 1.0f), 500, Easing.OutElasticHalf); 58 | } 59 | 60 | public override void Hide() 61 | { 62 | this.FadeOut(100) 63 | .OnComplete(_ => 64 | { 65 | if (Parent is NotificationOverlay container) 66 | container.RemoveBigNotification(this); 67 | }); 68 | } 69 | 70 | protected override void Update() 71 | { 72 | if (_clock.ElapsedMilliseconds > _duration) 73 | Hide(); 74 | 75 | if (!_clock.IsRunning) 76 | _clock.Start(); 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Notification/Drawables/DrawableNotification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using osu.Framework.Allocation; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Graphics.Colour; 5 | using osu.Framework.Graphics.Containers; 6 | using osu.Framework.Graphics.Shapes; 7 | using osu.Framework.Input.Events; 8 | using osu.Framework.Localisation; 9 | using osu.Framework.Timing; 10 | using osuTK; 11 | using osuTK.Graphics; 12 | 13 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Notification.Drawables 14 | { 15 | public partial class DrawableNotification : CompositeDrawable 16 | { 17 | private readonly StopwatchClock _clock; 18 | private readonly Action _clickAction; 19 | private readonly ColourInfo _borderColour; 20 | private readonly TextFlowContainer _textFlowContainer; 21 | private readonly double _duration; 22 | 23 | /// 24 | /// Drawable Notification 25 | /// 26 | /// 27 | /// 28 | /// Hide after X amount of MS, -1 = PositiveInfinity 29 | /// 30 | public DrawableNotification(LocalisableString text, ColourInfo colourInfo, int duration = -1, Action clickAction = null) 31 | { 32 | Margin = new MarginPadding(6); 33 | 34 | _clock = new StopwatchClock(); 35 | _clickAction = clickAction; 36 | _borderColour = colourInfo; 37 | _textFlowContainer = new TextFlowContainer 38 | { 39 | Direction = FillDirection.Full, 40 | AutoSizeAxes = Axes.Both, 41 | MaximumSize = new Vector2(290, float.MaxValue), 42 | Padding = new MarginPadding(10) 43 | }; 44 | _duration = duration == -1 ? Math.Max(3000, (double) (text.ToString().Length * 100)) : duration; 45 | 46 | _textFlowContainer.AddText(text.ToString()); 47 | } 48 | 49 | [BackgroundDependencyLoader] 50 | private void Load() 51 | { 52 | Masking = true; 53 | BorderThickness = 2; 54 | BorderColour = _borderColour; 55 | CornerRadius = 8; 56 | AutoSizeAxes = Axes.Y; 57 | Width = 300; 58 | 59 | AddInternal(new Box 60 | { 61 | Colour = new Color4(0f,0f,0f,.8f), 62 | RelativeSizeAxes = Axes.Both 63 | }); 64 | AddInternal(_textFlowContainer); 65 | } 66 | 67 | public void FadeBorder(ColourInfo newColour, double duration = 0, Easing easing = Easing.None) 68 | => this.TransformTo(nameof(BorderColour), (ColourInfo)newColour.AverageColour, duration, easing); 69 | 70 | protected override bool OnHover(HoverEvent e) 71 | { 72 | FadeBorder(Color4.White, 100); 73 | 74 | _clock.Stop(); 75 | 76 | return true; 77 | } 78 | 79 | protected override void OnHoverLost(HoverLostEvent e) 80 | { 81 | FadeBorder(_borderColour, 100); 82 | 83 | if (_clock.ElapsedMilliseconds > _duration) 84 | OnClick(null); 85 | } 86 | 87 | private bool _gotClicked; 88 | 89 | protected override bool OnClick(ClickEvent e) 90 | { 91 | if (_gotClicked) 92 | return false; 93 | 94 | if (e != null) 95 | _clickAction?.Invoke(); 96 | 97 | _gotClicked = true; 98 | 99 | this.FadeOutFromOne(250).Finally(_ => 100 | { 101 | if (Parent is FillFlowContainer container) 102 | container.Remove(this, true); 103 | }); 104 | 105 | return true; 106 | } 107 | 108 | protected override void Update() 109 | { 110 | if (_clock.ElapsedMilliseconds > _duration) 111 | OnClick(null); 112 | 113 | if (!_clock.IsRunning && !IsHovered) 114 | _clock.Start(); 115 | 116 | base.Update(); 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Notification/NotificationOverlay.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using osu.Framework.Allocation; 4 | using osu.Framework.Graphics; 5 | using osu.Framework.Graphics.Colour; 6 | using osu.Framework.Graphics.Containers; 7 | using osu.Framework.Localisation; 8 | using Qsor.Game.Graphics.UserInterface.Overlays.Notification.Drawables; 9 | 10 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Notification 11 | { 12 | public partial class NotificationOverlay : CompositeDrawable 13 | { 14 | private FillFlowContainer _drawableNotifications; 15 | 16 | [BackgroundDependencyLoader] 17 | private void Load() 18 | { 19 | RelativeSizeAxes = Axes.Both; 20 | 21 | Anchor = Anchor.TopRight; 22 | Origin = Anchor.TopRight; 23 | 24 | _drawableNotifications = new FillFlowContainer 25 | { 26 | Anchor = Anchor.TopRight, 27 | Origin = Anchor.TopRight, 28 | 29 | Direction = FillDirection.Vertical, 30 | AutoSizeAxes = Axes.X, 31 | RelativeSizeAxes = Axes.Y, 32 | LayoutEasing = Easing.OutQuint, 33 | LayoutDuration = 400 34 | }; 35 | 36 | //Padding = new MarginPadding(10); 37 | 38 | AddInternal(_drawableNotifications); 39 | } 40 | 41 | public void AddNotification(LocalisableString text, ColourInfo colourInfo, int duration = -1, Action clickAction = null) 42 | => AddNotification(new DrawableNotification(text, colourInfo, duration, clickAction)); 43 | 44 | public void AddBigNotification(LocalisableString text, int duration = -1) 45 | { 46 | Scheduler.AddOnce(() => 47 | { 48 | var notification = new DrawableBigNotification(text, duration) 49 | { 50 | Anchor = Anchor.Centre, 51 | Origin = Anchor.Centre 52 | }; 53 | 54 | AddInternal(notification); 55 | 56 | notification.Show(); 57 | }); 58 | } 59 | 60 | public void AddNotification(DrawableNotification notification) 61 | { 62 | Scheduler.AddOnce(() => 63 | { 64 | notification.Anchor = Anchor.BottomRight; 65 | notification.Origin = Anchor.BottomRight; 66 | notification.Alpha = 0; 67 | 68 | _drawableNotifications.Add(notification); 69 | _drawableNotifications.SetLayoutPosition(notification, -_drawableNotifications.FlowingChildren.Count()); 70 | 71 | notification.FadeInFromZero(200); 72 | }); 73 | } 74 | 75 | internal void RemoveBigNotification(DrawableBigNotification notification) 76 | { 77 | RemoveInternal(notification, true); 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/Categories/SettingsAudioCategory.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Sprites; 2 | 3 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings.Categories 4 | { 5 | public partial class SettingsAudioCategory : SettingsCategoryContainer 6 | { 7 | public override string Name => "Audio"; 8 | public override IconUsage Icon => FontAwesome.Solid.AudioDescription; 9 | } 10 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/Categories/SettingsEditorCategory.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Sprites; 2 | 3 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings.Categories 4 | { 5 | public partial class SettingsEditorCategory : SettingsCategoryContainer 6 | { 7 | public override string Name => "Maintenance"; 8 | public override IconUsage Icon => FontAwesome.Solid.PencilAlt; 9 | } 10 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/Categories/SettingsGameplayCategory.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Sprites; 2 | 3 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings.Categories 4 | { 5 | public partial class SettingsGameplayCategory : SettingsCategoryContainer 6 | { 7 | public override string Name => "Gameplay"; 8 | public override IconUsage Icon => FontAwesome.Regular.Circle; 9 | } 10 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/Categories/SettingsGeneralCategory.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics.Sprites; 3 | using Qsor.Game.Graphics.UserInterface.Overlays.Settings.Drawables; 4 | using Qsor.Game.Graphics.UserInterface.Overlays.Settings.Drawables.Objects; 5 | 6 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings.Categories 7 | { 8 | public partial class SettingsGeneralCategory : SettingsCategoryContainer 9 | { 10 | public override string Name => "General"; 11 | public override IconUsage Icon => FontAwesome.Solid.Cog; 12 | 13 | [BackgroundDependencyLoader] 14 | private void Load() 15 | { 16 | var signIn = new DrawableSettingsSubCategory("SIGN IN"); 17 | signIn.Content.Add(new DrawableSettingsInput("", "Username", "Enter your username")); 18 | signIn.Content.Add(new DrawableSettingsInput("false", "Password", "Enter your password", true)); 19 | signIn.Content.Add(new DrawableSettingsCheckbox(true, "Remember Username", "Remember the username next time Qsor starts.")); 20 | signIn.Content.Add(new DrawableSettingsCheckbox(false, "Remember Password", "Remember the password next time Qsor starts.")); 21 | 22 | AddInternal(signIn); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/Categories/SettingsGraphicsCategory.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics.Sprites; 3 | 4 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings.Categories 5 | { 6 | public partial class SettingsGraphicsCategory : SettingsCategoryContainer 7 | { 8 | public override string Name => "Graphics"; 9 | public override IconUsage Icon => FontAwesome.Solid.Desktop; 10 | 11 | [BackgroundDependencyLoader] 12 | private void Load() 13 | { 14 | 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/Categories/SettingsInputCategory.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Sprites; 2 | 3 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings.Categories 4 | { 5 | public partial class SettingsInputCategory : SettingsCategoryContainer 6 | { 7 | public override string Name => "Input"; 8 | public override IconUsage Icon => FontAwesome.Solid.Gamepad; 9 | } 10 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/Categories/SettingsMaintenanceCategory.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Sprites; 2 | 3 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings.Categories 4 | { 5 | public partial class SettingsMaintenanceCategory : SettingsCategoryContainer 6 | { 7 | public override string Name => "Maintenance"; 8 | public override IconUsage Icon => FontAwesome.Solid.Wrench; 9 | } 10 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/Categories/SettingsOnlineCategory.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Sprites; 2 | 3 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings.Categories 4 | { 5 | public partial class SettingsOnlineCategory : SettingsCategoryContainer 6 | { 7 | public override string Name => "Online"; 8 | public override IconUsage Icon => FontAwesome.Solid.GlobeAmericas; 9 | } 10 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/Categories/SettingsSkinCategory.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Sprites; 2 | 3 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings.Categories 4 | { 5 | public partial class SettingsSkinCategory : SettingsCategoryContainer 6 | { 7 | public override string Name => "Skin"; 8 | public override IconUsage Icon => FontAwesome.Solid.PaintBrush; 9 | } 10 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/Drawables/DrawableSettingsCategory.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Containers; 4 | using osu.Framework.Graphics.Sprites; 5 | using osuTK.Graphics; 6 | 7 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings.Drawables 8 | { 9 | public partial class DrawableSettingsCategory : FillFlowContainer 10 | { 11 | private SpriteText _text; 12 | 13 | private readonly SettingsCategoryContainer _categoryContainer; 14 | 15 | public DrawableSettingsCategory(SettingsCategoryContainer category) 16 | { 17 | _categoryContainer = category; 18 | } 19 | 20 | [BackgroundDependencyLoader] 21 | private void Load() 22 | { 23 | _text = new SpriteText 24 | { 25 | Text = Name.ToUpper(), 26 | Colour = Color4.SkyBlue, 27 | Font = new FontUsage(size: 24, weight: "Bold"), 28 | Anchor = Anchor.TopRight, 29 | Origin = Anchor.TopRight 30 | }; 31 | 32 | Direction = FillDirection.Vertical; 33 | 34 | Add(_text); 35 | 36 | Add(_categoryContainer); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/Drawables/DrawableSettingsIconSprite.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Graphics.Containers; 5 | using osu.Framework.Graphics.Shapes; 6 | using osu.Framework.Graphics.Sprites; 7 | using osu.Framework.Input.Events; 8 | using osuTK.Graphics; 9 | 10 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings.Drawables 11 | { 12 | public partial class DrawableSettingsIconSprite : CompositeDrawable 13 | { 14 | 15 | [Resolved] 16 | private DrawableSettingsToolBar ToolBar { get; set; } 17 | 18 | private readonly Bindable _selectedCategory = new(); 19 | 20 | private readonly SpriteIcon _icon = new(); 21 | private Box _selector; 22 | 23 | 24 | public IconUsage Icon 25 | { 26 | get => _icon.Icon; 27 | set => _icon.Icon = value; 28 | } 29 | 30 | public SettingsCategoryContainer Category { get; } 31 | 32 | public DrawableSettingsIconSprite(SettingsCategoryContainer category) 33 | { 34 | Category = category; 35 | } 36 | 37 | [BackgroundDependencyLoader] 38 | private void Load(SettingsOverlay settingsOverlay) 39 | { 40 | _selectedCategory.BindTo(settingsOverlay.SelectedCategory); 41 | CornerRadius = 0; 42 | 43 | Width = 48; 44 | Height = 48; 45 | 46 | AddInternal(_icon); 47 | _icon.Width = 24; 48 | _icon.Height = 24; 49 | _icon.Origin = Anchor.Centre; 50 | _icon.Anchor = Anchor.Centre; 51 | _icon.Colour = Color4.Gray; 52 | 53 | AddInternal(_selector = new Box 54 | { 55 | Colour = Color4.HotPink, 56 | Anchor = Anchor.CentreRight, 57 | Origin = Anchor.Centre, 58 | Width = 4, 59 | Height = 48, 60 | Margin = new MarginPadding { Right = 2.5f }, 61 | Alpha = 0, 62 | }); 63 | 64 | _selectedCategory.ValueChanged += e => 65 | { 66 | _icon.FadeColour(e.NewValue == Category ? Color4.White : Color4.Gray, 100); 67 | _selector.FadeTo(e.NewValue == Category ? 1f : 0f, 250); 68 | }; 69 | } 70 | 71 | protected override bool OnHover(HoverEvent e) 72 | { 73 | if (_selectedCategory.Value == Category) 74 | return false; 75 | 76 | _icon.FadeColour(Color4.White, 100); 77 | return true; 78 | } 79 | 80 | protected override void OnHoverLost(HoverLostEvent e) 81 | { 82 | if (_selectedCategory.Value != Category) 83 | _icon.FadeColour(Color4.Gray, 100); 84 | } 85 | 86 | protected override bool OnClick(ClickEvent e) 87 | { 88 | _selectedCategory.Value = Category; 89 | return true; 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/Drawables/DrawableSettingsMenu.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Specialized; 2 | using System.Linq; 3 | using osu.Framework.Allocation; 4 | using osu.Framework.Bindables; 5 | using osu.Framework.Graphics; 6 | using osu.Framework.Graphics.Containers; 7 | using osu.Framework.Graphics.Shapes; 8 | using osu.Framework.Graphics.Sprites; 9 | using osuTK.Graphics; 10 | 11 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings.Drawables 12 | { 13 | [Cached] 14 | public partial class DrawableSettingsMenu : CompositeDrawable 15 | { 16 | private readonly BindableList _categories = new(); 17 | private SearchContainer _searchContainer; 18 | private BasicScrollContainer _scrollContainer; 19 | 20 | public DrawableSettingsMenu(BindableList categories) 21 | { 22 | _categories.BindTo(categories); 23 | } 24 | 25 | [BackgroundDependencyLoader] 26 | private void Load(SettingsOverlay settingsOverlay) 27 | { 28 | Masking = true; 29 | RelativeSizeAxes = Axes.Y; 30 | 31 | Margin = new MarginPadding{ Left = 48 }; 32 | Width = 400; 33 | 34 | AddInternal(new Box 35 | { 36 | RelativeSizeAxes = Axes.Both, 37 | Colour = Color4.Black, 38 | Alpha = .6f 39 | }); 40 | 41 | AddInternal(_scrollContainer = new BasicScrollContainer 42 | { 43 | RelativeSizeAxes = Axes.Both, 44 | ScrollbarVisible = true 45 | }); 46 | 47 | var headerContainer = new CustomizableTextContainer 48 | { 49 | Anchor = Anchor.TopCentre, 50 | Origin = Anchor.TopCentre, 51 | TextAnchor = Anchor.TopCentre, 52 | //RelativeSizeAxes = Axes.X, 53 | Width = 400, 54 | AutoSizeAxes = Axes.Y, 55 | 56 | Margin = new MarginPadding{ Top = 50 } 57 | }; 58 | 59 | headerContainer.AddPlaceholder(new SpriteIcon 60 | { 61 | Width = 24, 62 | Height = 24, 63 | Icon = FontAwesome.Solid.Search 64 | }); 65 | 66 | headerContainer.AddText("Options\n", e => e.Font = new FontUsage(size: 24, weight: "Bold")); 67 | headerContainer.AddText("Change the way Qsor behaves\n\n\n\n", e => { e.Colour = Color4.PaleVioletRed; e.Font = new FontUsage(size: 18);}); 68 | headerContainer.AddText("[0] Type to search!", e => e.Font = new FontUsage(size: 24)); 69 | 70 | _scrollContainer.ScrollContent.Add(headerContainer); 71 | 72 | _scrollContainer.ScrollbarVisible = false; 73 | _scrollContainer.ScrollContent.AutoSizeAxes = Axes.Y; 74 | _scrollContainer.ScrollContent.RelativeSizeAxes = Axes.X; 75 | 76 | _scrollContainer.ScrollContent.Add(_searchContainer = new SearchContainer 77 | { 78 | RelativeSizeAxes = Axes.X, 79 | AutoSizeAxes = Axes.Y, 80 | Margin = new MarginPadding{ Top = 150 }, 81 | }); 82 | 83 | settingsOverlay.SelectedCategory.ValueChanged += e => 84 | { 85 | // Little hack to make it feel smoother 86 | if (_categories.FirstOrDefault() == e.NewValue) 87 | { 88 | _scrollContainer.ScrollToStart(); 89 | } 90 | else if (_categories.LastOrDefault() == e.NewValue) 91 | { 92 | _scrollContainer.ScrollToEnd(); 93 | } 94 | else 95 | { 96 | _scrollContainer.ScrollTo(e.NewValue); 97 | } 98 | }; 99 | 100 | _categories.CollectionChanged += (_, e) => 101 | { 102 | if (e.Action == NotifyCollectionChangedAction.Add) 103 | { 104 | foreach (var i in e.NewItems) 105 | { 106 | var item = (SettingsCategoryContainer) i; 107 | 108 | var settingsCategory = new DrawableSettingsCategory(item) 109 | { 110 | Name = item.Name, 111 | Anchor = Anchor.TopLeft, 112 | Padding = new MarginPadding(20), 113 | RelativeSizeAxes = Axes.X, 114 | AutoSizeAxes = Axes.Y 115 | }; 116 | 117 | _searchContainer.Add(settingsCategory); 118 | } 119 | } 120 | }; 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/Drawables/DrawableSettingsSubCategory.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Graphics.Containers; 5 | using osu.Framework.Graphics.Shapes; 6 | using osu.Framework.Graphics.Sprites; 7 | using osu.Framework.Localisation; 8 | using osuTK; 9 | 10 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings.Drawables 11 | { 12 | public partial class DrawableSettingsSubCategory : CompositeDrawable 13 | { 14 | public Bindable Label = new(string.Empty); 15 | private SpriteText _label; 16 | 17 | public FillFlowContainer Content { get; } = new FillFlowContainer 18 | { 19 | RelativeSizeAxes = Axes.X, 20 | AutoSizeAxes = Axes.Y, 21 | Spacing = new Vector2(0, 2.5f), 22 | Margin = new MarginPadding { Left = 10, Top = 50 } 23 | }; 24 | 25 | [BackgroundDependencyLoader] 26 | private void Load() 27 | { 28 | RelativeSizeAxes = Axes.X; 29 | AutoSizeAxes = Axes.Y; 30 | 31 | AddInternal(new Box 32 | { 33 | RelativeSizeAxes = Axes.Y, 34 | Width = 2, 35 | Origin = Anchor.Centre, 36 | Anchor = Anchor.CentreLeft, 37 | Alpha = .2f 38 | }); 39 | AddInternal(_label = new SpriteText 40 | { 41 | Text = Label.Value, 42 | Font = new FontUsage(size: 22, weight: "bold"), 43 | Margin = new MarginPadding{ Left = 10, Top = 5 } 44 | }); 45 | 46 | Label.ValueChanged += e => _label.Text = e.NewValue; 47 | AddInternal(Content); 48 | } 49 | 50 | public DrawableSettingsSubCategory(LocalisableString label) 51 | { 52 | Label.Default = label; 53 | Label.SetDefault(); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/Drawables/DrawableSettingsToolBar.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Specialized; 2 | using System.Linq; 3 | using osu.Framework.Allocation; 4 | using osu.Framework.Bindables; 5 | using osu.Framework.Graphics; 6 | using osu.Framework.Graphics.Containers; 7 | using osu.Framework.Graphics.Shapes; 8 | using osuTK.Graphics; 9 | 10 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings.Drawables 11 | { 12 | [Cached] 13 | public partial class DrawableSettingsToolBar : CompositeDrawable 14 | { 15 | private readonly BindableList _categories = new(); 16 | private BasicScrollContainer _scrollContainer; 17 | private SearchContainer _iconFlowContainer; 18 | 19 | private readonly Bindable _selectedCategory = new(); 20 | 21 | public DrawableSettingsToolBar(BindableList categories) 22 | { 23 | _categories.BindTo(categories); 24 | } 25 | 26 | [BackgroundDependencyLoader] 27 | private void Load(SettingsOverlay settingsOverlay) 28 | { 29 | _selectedCategory.BindTo(settingsOverlay.SelectedCategory); 30 | 31 | RelativeSizeAxes = Axes.Y; 32 | Width = 48; 33 | 34 | AddInternal(new Box 35 | { 36 | RelativeSizeAxes = Axes.Both, 37 | Colour = Color4.Black 38 | }); 39 | 40 | AddInternal(_scrollContainer = new BasicScrollContainer 41 | { 42 | RelativeSizeAxes = Axes.Both, 43 | }); 44 | 45 | _scrollContainer.ScrollbarVisible = false; 46 | _scrollContainer.ScrollContent.AutoSizeAxes = Axes.Y; 47 | _scrollContainer.ScrollContent.RelativeSizeAxes = Axes.X; 48 | _scrollContainer.ScrollContent.Origin = Anchor.Centre; 49 | _scrollContainer.ScrollContent.Anchor = Anchor.Centre; 50 | 51 | _scrollContainer.ScrollContent.Add(_iconFlowContainer = new SearchContainer 52 | { 53 | RelativeSizeAxes = Axes.X, 54 | AutoSizeAxes = Axes.Y, 55 | Direction = FillDirection.Vertical 56 | }); 57 | 58 | _categories.CollectionChanged += (_, e) => 59 | { 60 | if (e.Action == NotifyCollectionChangedAction.Add) 61 | { 62 | foreach (var i in e.NewItems) 63 | { 64 | var item = (SettingsCategoryContainer) i; 65 | 66 | var settingsIconSprite = new DrawableSettingsIconSprite(item) 67 | { 68 | Name = item.Name, 69 | Icon = item.Icon, 70 | Origin = Anchor.Centre, 71 | Anchor = Anchor.Centre, 72 | Colour = Color4.LightGray, 73 | }; 74 | 75 | _iconFlowContainer.Add(settingsIconSprite); 76 | } 77 | } 78 | }; 79 | } 80 | 81 | public void Default() 82 | { 83 | var sprite = _iconFlowContainer.FirstOrDefault(s => s.Name == "General"); 84 | if (sprite == null) 85 | return; 86 | 87 | _selectedCategory.Value = sprite.Category; 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/Drawables/Objects/DrawableSettingsCheckbox.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Graphics.Containers; 5 | using osu.Framework.Graphics.Shapes; 6 | using osu.Framework.Graphics.Sprites; 7 | using osu.Framework.Input.Events; 8 | using osu.Framework.Localisation; 9 | using osuTK.Graphics; 10 | 11 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings.Drawables.Objects 12 | { 13 | public partial class DrawableSettingsCheckbox : DrawableSettingsObject 14 | { 15 | private SpriteText _label; 16 | 17 | [BackgroundDependencyLoader] 18 | private void Load() 19 | { 20 | Origin = Anchor.Centre; 21 | Anchor = Anchor.Centre; 22 | 23 | AddInternal(new DrawableSettingsCheckboxNode(Value)); 24 | AddInternal(_label = new SpriteText 25 | { 26 | Origin = Anchor.CentreLeft, 27 | 28 | Margin = new MarginPadding{ Left = 18f }, 29 | 30 | Font = new FontUsage(size: 18), 31 | Text = Label.Value 32 | }); 33 | 34 | Label.ValueChanged += e => _label.Text = e.NewValue; 35 | } 36 | 37 | public DrawableSettingsCheckbox(bool defaultValue, LocalisableString label, LocalisableString toolTip) : base(defaultValue, label, toolTip) 38 | { 39 | } 40 | 41 | protected override bool OnClick(ClickEvent e) 42 | { 43 | if (Value.Disabled) 44 | return false; 45 | 46 | Value.Value = !Value.Value; 47 | return true; 48 | } 49 | 50 | private partial class DrawableSettingsCheckboxNode : CompositeDrawable 51 | { 52 | public readonly Bindable Checked = new(); 53 | 54 | public DrawableSettingsCheckboxNode(Bindable checkedBindable) 55 | { 56 | Checked.BindTo(checkedBindable); 57 | } 58 | 59 | private Box _box; 60 | 61 | [BackgroundDependencyLoader] 62 | private void Load() 63 | { 64 | Width = 16; 65 | Height = 16; 66 | 67 | Origin = Anchor.Centre; 68 | 69 | Masking = true; 70 | BorderColour = Color4.PaleVioletRed; 71 | BorderThickness = 2.5f; 72 | 73 | CornerRadius = Width / 2f; 74 | 75 | AddInternal(_box = new Box 76 | { 77 | RelativeSizeAxes = Axes.Both, 78 | Colour = Color4.PaleVioletRed, 79 | 80 | Alpha = Checked.Value ? 1 : 0, 81 | AlwaysPresent = true 82 | }); 83 | 84 | Checked.ValueChanged += e => _box.FadeTo(e.NewValue ? 1 : 0, 100, Easing.In); 85 | Checked.DisabledChanged += e => 86 | { 87 | _box.Colour = e ? Color4.DimGray : Color4.PaleVioletRed; 88 | BorderColour = e ? Color4.Gray : Color4.PaleVioletRed; 89 | }; 90 | } 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/Drawables/Objects/DrawableSettingsInput.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Graphics.Colour; 5 | using osu.Framework.Graphics.Shapes; 6 | using osu.Framework.Graphics.Sprites; 7 | using osu.Framework.Graphics.UserInterface; 8 | using osu.Framework.Input.Events; 9 | using osu.Framework.Localisation; 10 | using osu.Framework.Logging; 11 | using osuTK; 12 | using osuTK.Graphics; 13 | 14 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings.Drawables.Objects 15 | { 16 | public partial class DrawableSettingsInput : DrawableSettingsObject 17 | { 18 | private bool _isPassword; 19 | private DrawableSettingsInputNode _node; 20 | private SpriteText _label; 21 | 22 | [BackgroundDependencyLoader] 23 | private void Load() 24 | { 25 | Origin = Anchor.Centre; 26 | Anchor = Anchor.Centre; 27 | 28 | Height = 64; 29 | 30 | AddInternal(_label = new SpriteText 31 | { 32 | Origin = Anchor.CentreLeft, 33 | 34 | Font = new FontUsage(size: 18), 35 | Text = Label.Value 36 | }); 37 | 38 | AddInternal(_node = new DrawableSettingsInputNode(Value, _isPassword) 39 | { 40 | RelativeSizeAxes = Axes.X, 41 | Position = new Vector2(-2, 16), 42 | Height = 22 43 | }); 44 | 45 | Label.ValueChanged += e => _label.Text = e.NewValue; 46 | } 47 | 48 | 49 | public override bool AcceptsFocus => true; 50 | protected override void OnFocus(FocusEvent e) 51 | { 52 | Logger.LogPrint("Focus!!"); 53 | base.OnFocus(e); 54 | GetContainingInputManager().ChangeFocus(null); 55 | GetContainingInputManager().ChangeFocus(_node); 56 | } 57 | 58 | public DrawableSettingsInput(string defaultValue, LocalisableString label, LocalisableString toolTip, 59 | bool isPassword = false) 60 | : base(defaultValue, label, toolTip) 61 | { 62 | _isPassword = isPassword; 63 | } 64 | 65 | private partial class DrawableSettingsInputNode : TextBox 66 | { 67 | private readonly bool _isPassword; 68 | 69 | protected override bool AllowClipboardExport => !_isPassword; 70 | protected override bool AllowWordNavigation => !_isPassword; 71 | protected override Drawable AddCharacterToFlow(char c) => 72 | base.AddCharacterToFlow(_isPassword ? '*' : c); 73 | 74 | public DrawableSettingsInputNode(Bindable inputBox, bool isPassword) 75 | { 76 | _isPassword = isPassword; 77 | } 78 | 79 | [BackgroundDependencyLoader] 80 | private void Load() 81 | { 82 | Masking = true; 83 | 84 | //CornerRadius = 3; 85 | //CornerExponent = 2f; 86 | 87 | BorderColour = Color4.DarkGray; 88 | BorderThickness = 3f; 89 | 90 | AddInternal(new Box 91 | { 92 | Colour = Color4.Transparent, 93 | RelativeSizeAxes = Axes.Both, 94 | }); 95 | } 96 | 97 | protected override void NotifyInputError() 98 | { 99 | } 100 | 101 | protected override SpriteText CreatePlaceholder() 102 | { 103 | return new SpriteText(); 104 | } 105 | 106 | protected override Caret CreateCaret() 107 | { 108 | return new BasicTextBox.BasicCaret(); 109 | } 110 | 111 | protected override void OnFocus(FocusEvent e) 112 | { 113 | this.TransformTo(nameof(BorderColour), (ColourInfo)Color4.White, 100); 114 | base.OnFocus(e); 115 | } 116 | 117 | protected override void OnFocusLost(FocusLostEvent e) 118 | { 119 | this.TransformTo(nameof(BorderColour), (ColourInfo)Color4.DarkGray, 100); 120 | base.OnFocusLost(e); 121 | } 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/Drawables/Objects/DrawableSettingsLabel.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Sprites; 4 | using osu.Framework.Localisation; 5 | 6 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings.Drawables.Objects 7 | { 8 | public partial class DrawableSettingsLabel : DrawableSettingsObject 9 | { 10 | private SpriteText _label; 11 | 12 | [BackgroundDependencyLoader] 13 | private void Load() 14 | { 15 | Label.BindTo(Value); 16 | 17 | AddInternal(_label = new SpriteText 18 | { 19 | Origin = Anchor.CentreLeft, 20 | 21 | Font = new FontUsage(size: 18), 22 | 23 | Text = Label.Value 24 | }); 25 | 26 | Label.ValueChanged += e => _label.Text = e.NewValue; 27 | } 28 | 29 | public DrawableSettingsLabel(LocalisableString defaultValue, LocalisableString toolTip) : base(defaultValue, string.Empty, toolTip) 30 | { 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/Drawables/Objects/DrawableSettingsObject.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Graphics.Containers; 5 | using osu.Framework.Graphics.Cursor; 6 | using osu.Framework.Input; 7 | using osu.Framework.Input.Events; 8 | using osu.Framework.Localisation; 9 | 10 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings.Drawables.Objects 11 | { 12 | public abstract partial class DrawableSettingsObject : CompositeDrawable, IRequireHighFrequencyMousePosition, IHasTooltip 13 | { 14 | public readonly Bindable Label = new(string.Empty); 15 | public readonly Bindable ToolTip = new(string.Empty); 16 | public readonly Bindable Value; 17 | 18 | public LocalisableString TooltipText => ToolTip.Value; 19 | 20 | [Resolved] 21 | private SettingsOverlay SettingsOverlay { get; set; } 22 | 23 | public DrawableSettingsObject(T defaultValue, LocalisableString label, LocalisableString toolTip) 24 | { 25 | Label.Value = label; 26 | ToolTip.Value = toolTip; 27 | 28 | Value = new Bindable(defaultValue); 29 | 30 | Value.SetDefault(); 31 | } 32 | 33 | [BackgroundDependencyLoader] 34 | private void Load() 35 | { 36 | RelativeSizeAxes = Axes.X; 37 | 38 | Height = 32; 39 | 40 | Padding = new MarginPadding(10); 41 | } 42 | 43 | protected override bool OnMouseMove(MouseMoveEvent e) 44 | { 45 | if (!IsHovered) 46 | return base.OnMouseMove(e); 47 | 48 | SettingsOverlay.MoveIndexTo(this); 49 | return true; 50 | } 51 | 52 | protected override bool OnHover(HoverEvent e) 53 | { 54 | SettingsOverlay.ObjectHovering(); 55 | return true; 56 | } 57 | 58 | protected override void OnHoverLost(HoverLostEvent e) 59 | { 60 | SettingsOverlay.ObjectHoverLost(); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/SettingsCategoryContainer.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Containers; 3 | using osu.Framework.Graphics.Sprites; 4 | 5 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings 6 | { 7 | public abstract partial class SettingsCategoryContainer : Container 8 | { 9 | public new abstract string Name { get; } 10 | public abstract IconUsage Icon { get; } 11 | 12 | public SettingsCategoryContainer() 13 | { 14 | RelativeSizeAxes = Axes.X; 15 | AutoSizeAxes = Axes.Y; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/Settings/SettingsOverlay.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Graphics.Containers; 5 | using osu.Framework.Graphics.Shapes; 6 | using osuTK.Graphics; 7 | using Qsor.Game.Graphics.UserInterface.Overlays.Settings.Categories; 8 | using Qsor.Game.Graphics.UserInterface.Overlays.Settings.Drawables; 9 | 10 | namespace Qsor.Game.Graphics.UserInterface.Overlays.Settings 11 | { 12 | [Cached] 13 | public partial class SettingsOverlay : CompositeDrawable 14 | { 15 | private readonly BindableList _categories = new(); 16 | 17 | private DrawableSettingsToolBar _toolBar; 18 | private DrawableSettingsMenu _menu; 19 | 20 | public readonly Bindable SelectedCategory = new(); 21 | 22 | private Box _settingsIndex; 23 | 24 | [BackgroundDependencyLoader] 25 | private void Load() 26 | { 27 | RelativeSizeAxes = Axes.Y; 28 | AutoSizeAxes = Axes.X; 29 | 30 | AddInternal(_settingsIndex = new Box 31 | { 32 | Anchor = Anchor.TopCentre, 33 | Origin = Anchor.Centre, 34 | 35 | Colour = Color4.Black, 36 | RelativeSizeAxes = Axes.X, 37 | Alpha = 0 38 | }); 39 | 40 | AddInternal(_menu = new DrawableSettingsMenu(_categories)); 41 | AddInternal(_toolBar = new DrawableSettingsToolBar(_categories)); 42 | 43 | AddCategory(new SettingsGeneralCategory()); 44 | AddCategory(new SettingsGraphicsCategory()); 45 | AddCategory(new SettingsGameplayCategory()); 46 | AddCategory(new SettingsSkinCategory()); 47 | AddCategory(new SettingsInputCategory()); 48 | AddCategory(new SettingsEditorCategory()); 49 | AddCategory(new SettingsOnlineCategory()); 50 | AddCategory(new SettingsMaintenanceCategory()); 51 | 52 | _menu.Width = 0; 53 | Alpha = 0; 54 | } 55 | 56 | protected override void LoadComplete() 57 | { 58 | _toolBar.Default(); 59 | } 60 | 61 | public void AddCategory(SettingsCategoryContainer category) 62 | { 63 | _categories.Add(category); 64 | } 65 | 66 | public bool IsShown { get; private set; } 67 | public override void Show() 68 | { 69 | IsShown = true; 70 | 71 | this.FadeIn(200); 72 | _menu.ResizeWidthTo(400, 400, Easing.InOutCubic); 73 | } 74 | 75 | public override void Hide() 76 | { 77 | IsShown = false; 78 | 79 | this.FadeOut(800); 80 | _menu.ResizeWidthTo(0, 1000, Easing.InOutCubic); 81 | } 82 | 83 | private int _childHoverLength; 84 | 85 | internal void ObjectHovering() 86 | { 87 | if (_childHoverLength <= 0) 88 | { 89 | _settingsIndex.FadeIn(100); 90 | } 91 | 92 | _childHoverLength++; 93 | } 94 | 95 | internal void ObjectHoverLost() 96 | { 97 | _childHoverLength--; 98 | if (_childHoverLength > 0) 99 | return; 100 | 101 | _childHoverLength = 0; 102 | _settingsIndex.FadeOut(500); 103 | } 104 | 105 | private object _movingTo; 106 | public void MoveIndexTo(Drawable settingsObject) 107 | { 108 | if (_movingTo == settingsObject) 109 | return; 110 | 111 | _movingTo = settingsObject; 112 | 113 | var pos = settingsObject.ToSpaceOfOtherDrawable(settingsObject.OriginPosition, _menu); 114 | 115 | // -5f because of 10 padding around the settings object. 116 | _settingsIndex.MoveToY(pos.Y - 5f, 100, Easing.OutBack); 117 | _settingsIndex.ResizeHeightTo(settingsObject.DrawSize.Y, 140, Easing.OutBack); 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/UpdaterOverlay.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Containers; 4 | using osu.Framework.Graphics.Sprites; 5 | using osu.Framework.Input.Events; 6 | using osu.Framework.Localisation; 7 | using osuTK; 8 | using Qsor.Game.Updater; 9 | 10 | namespace Qsor.Game.Graphics.UserInterface.Overlays 11 | { 12 | public partial class UpdaterOverlay : CompositeDrawable 13 | { 14 | private SpriteText _statusText; 15 | private SpriteIcon _spinningIcon; 16 | 17 | public LocalisableString Text { get => _statusText.Text; set => _statusText.Text = value; } 18 | 19 | [Resolved] 20 | private UpdateManager UpdateManager { get; set; } 21 | 22 | [BackgroundDependencyLoader] 23 | private void Load() 24 | { 25 | Anchor = Origin = Anchor.BottomCentre; 26 | AutoSizeAxes = Axes.Both; 27 | 28 | Padding = new MarginPadding(25); 29 | Alpha = 0; 30 | 31 | AddInternal(_spinningIcon = new SpriteIcon 32 | { 33 | Icon = FontAwesome.Solid.SyncAlt, 34 | Size = new Vector2(32), 35 | Origin = Anchor.Centre, 36 | Anchor = Anchor.TopCentre, 37 | Rotation = 0 38 | }); 39 | 40 | _spinningIcon 41 | .RotateTo(360, 2500, Easing.InOutQuint) 42 | .Then() 43 | .RotateTo(0, 1) // 0 duration is not working ? 44 | .Loop(); 45 | 46 | AddInternal(_statusText = new SpriteText 47 | { 48 | Text = "Update available, click here to update!", 49 | Origin = Anchor.TopCentre, 50 | Anchor = Anchor.Centre, 51 | }); 52 | } 53 | 54 | protected override bool OnHover(HoverEvent e) 55 | { 56 | this.ScaleTo(1.25f, 500, Easing.OutBack); 57 | 58 | return true; 59 | } 60 | 61 | protected override void OnHoverLost(HoverLostEvent e) 62 | { 63 | this.ScaleTo(1f, 500, Easing.OutBack); 64 | } 65 | 66 | protected override bool OnClick(ClickEvent e) 67 | { 68 | UpdateManager.UpdateGame(); 69 | 70 | return true; 71 | } 72 | 73 | public override void Show() 74 | { 75 | this.FadeIn(2500, Easing.InOutQuint); 76 | } 77 | 78 | public override void Hide() 79 | { 80 | this.FadeOut(2500, Easing.InOutQuint); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Overlays/UserOverlay.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Containers; 4 | using osu.Framework.Graphics.Shapes; 5 | using osu.Framework.Graphics.Sprites; 6 | using osu.Framework.Input.Events; 7 | using osu.Framework.Localisation; 8 | using osuTK; 9 | using osuTK.Graphics; 10 | using Qsor.Game.Graphics.UserInterface.Online.Drawables; 11 | using Qsor.Game.Online; 12 | using Qsor.Game.Online.Users; 13 | 14 | namespace Qsor.Game.Graphics.UserInterface.Overlays 15 | { 16 | public partial class UserOverlay : CompositeDrawable 17 | { 18 | private TextFlowContainer _textFlowContainer; 19 | private DrawableAvatar _avatar; 20 | private DrawableLevelBar _levelBar; 21 | private User _activeUser; 22 | private Box _background; 23 | 24 | [BackgroundDependencyLoader] 25 | private void Load(UserManager userManager) 26 | { 27 | _activeUser = userManager.User; 28 | 29 | Width = 256; 30 | Height = 80; 31 | 32 | _textFlowContainer = new TextFlowContainer 33 | { 34 | AutoSizeAxes = Axes.Both, 35 | Padding = new MarginPadding(4), 36 | OriginPosition = new Vector2(-75,0) 37 | }; 38 | 39 | _textFlowContainer.AddParagraph($"{_activeUser.Username}", text => text.Font = new FontUsage(size: 24, weight: "Bold")); 40 | _textFlowContainer.AddParagraph($"Performance: {_activeUser.Statistics.PerformancePoints}pp", text => text.Font = new FontUsage(size: 14)); 41 | _textFlowContainer.AddParagraph($"Accuracy: {_activeUser.Statistics.Accuracy:0.00}%", text => text.Font = new FontUsage(size: 14)); 42 | _textFlowContainer.AddParagraph($"Level: {_activeUser.Statistics.GetLevel()}", text => text.Font = new FontUsage(size: 14)); 43 | 44 | AddRangeInternal(new Drawable[] 45 | { 46 | _background = new Box 47 | { 48 | RelativeSizeAxes = Axes.Both, 49 | Colour = Color4.White, 50 | Alpha = 0f 51 | }, 52 | _avatar = new DrawableAvatar(), 53 | new SpriteText 54 | { 55 | Font = new FontUsage(size: 32, weight: "Bold"), 56 | Colour = Color4.DarkGray, 57 | Text = new LocalisableString("#1"), 58 | 59 | Anchor = Anchor.BottomRight, 60 | Origin = Anchor.BottomRight, 61 | Padding = new MarginPadding(0), 62 | Position = new Vector2(0, -5) 63 | }, 64 | _textFlowContainer, 65 | _levelBar = new DrawableLevelBar(_activeUser) 66 | { 67 | Anchor = Anchor.BottomRight, 68 | Origin = Anchor.BottomRight, 69 | } 70 | }); 71 | 72 | _avatar.User.Value = _activeUser; 73 | } 74 | 75 | protected override bool OnHover(HoverEvent e) 76 | { 77 | _background.FadeTo(.3f, 50); 78 | return true; 79 | } 80 | 81 | protected override void OnHoverLost(HoverLostEvent e) 82 | { 83 | _background.FadeTo(0f, 50); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/QsorColours.cs: -------------------------------------------------------------------------------- 1 | namespace Qsor.Game.Graphics.UserInterface 2 | { 3 | /// 4 | /// Commonly used colours across the whole project 5 | /// 6 | public static class QsorColours 7 | { 8 | // TODO: Add commonly used colours here. 9 | } 10 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Screens/IntroScreen.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Containers; 4 | using osu.Framework.Graphics.Sprites; 5 | using osu.Framework.Screens; 6 | using osuTK; 7 | using osuTK.Graphics; 8 | 9 | namespace Qsor.Game.Graphics.UserInterface.Screens 10 | { 11 | public partial class IntroScreen : Screen 12 | { 13 | private SpriteIcon _spriteIcon; 14 | private CustomizableTextContainer _textFlowContainer; 15 | 16 | [BackgroundDependencyLoader] 17 | private void Load() 18 | { 19 | _textFlowContainer = new CustomizableTextContainer 20 | { 21 | Anchor = Anchor.Centre, 22 | Origin = Anchor.Centre, 23 | 24 | RelativeSizeAxes = Axes.Both, 25 | TextAnchor = Anchor.Centre, 26 | }; 27 | 28 | var warningIcon = _textFlowContainer.AddPlaceholder(_spriteIcon = new SpriteIcon 29 | { 30 | Icon = FontAwesome.Solid.ExclamationTriangle, 31 | Size = new Vector2(48), 32 | Colour = Color4.Yellow, 33 | }); 34 | 35 | _textFlowContainer.AddParagraph($"[{warningIcon}]"); 36 | _textFlowContainer.AddParagraph("Warning!", text => 37 | { 38 | text.Colour = Color4.Yellow; 39 | text.Font = new FontUsage(size: 32, weight: "Bold"); 40 | }); 41 | _textFlowContainer.AddParagraph("This game is currently in really early alpha! Bugs to be expected."); 42 | _textFlowContainer.AddParagraph("Please consider reporting them all, no matter how small they are."); 43 | 44 | AddInternal(_textFlowContainer); 45 | } 46 | 47 | public override void OnEntering(ScreenTransitionEvent e) 48 | { 49 | for (var i = 0; i < _textFlowContainer.Count; i++) 50 | _textFlowContainer[i].FadeInFromZero(250 + 100 * i); 51 | 52 | Scheduler.AddDelayed(() => _spriteIcon.FlashColour(Color4.White, 1000), 1000, true); 53 | } 54 | 55 | public override bool OnExiting(ScreenExitEvent e) 56 | { 57 | _textFlowContainer.FadeOutFromOne(500); 58 | 59 | return true; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Screens/MainMenu/Containers/BottomBar.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Containers; 4 | using osu.Framework.Graphics.Shapes; 5 | using osuTK.Graphics; 6 | 7 | namespace Qsor.Game.Graphics.UserInterface.Screens.MainMenu.Containers 8 | { 9 | public partial class BottomBar : CompositeDrawable 10 | { 11 | [BackgroundDependencyLoader] 12 | private void Load() 13 | { 14 | RelativeSizeAxes = Axes.X; 15 | FillMode = FillMode.Fit; 16 | Anchor = Anchor.BottomLeft; 17 | Origin = Anchor.BottomLeft; 18 | Height = 80; 19 | 20 | InternalChildren = new Drawable[] 21 | { 22 | new Box 23 | { 24 | Name = "Background", 25 | RelativeSizeAxes = Axes.Both, 26 | Colour = Color4.Black, 27 | Alpha = .4f 28 | } 29 | }; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Screens/MainMenu/Containers/Toolbar.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Containers; 4 | using osu.Framework.Graphics.Shapes; 5 | using osuTK.Graphics; 6 | using Qsor.Game.Graphics.UserInterface.Overlays; 7 | 8 | namespace Qsor.Game.Graphics.UserInterface.Screens.MainMenu.Containers 9 | { 10 | public partial class Toolbar : CompositeDrawable 11 | { 12 | [BackgroundDependencyLoader] 13 | private void Load() 14 | { 15 | RelativeSizeAxes = Axes.X; 16 | FillMode = FillMode.Fit; 17 | Height = 80; 18 | 19 | InternalChildren = new Drawable[] 20 | { 21 | new Box 22 | { 23 | Name = "Background", 24 | RelativeSizeAxes = Axes.Both, 25 | Colour = Color4.Black, 26 | Alpha = .4f 27 | }, 28 | 29 | new UserOverlay(), 30 | 31 | new MusicPlayerOverlay 32 | { 33 | Anchor = Anchor.TopRight, 34 | Origin = Anchor.TopRight, 35 | } 36 | }; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Screens/MainMenu/Drawables/DrawableLogoVisualisation.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. 2 | // See the LICENCE file in the repository root for full licence text. 3 | 4 | using System; 5 | using osu.Framework.Allocation; 6 | using osu.Framework.Bindables; 7 | using osu.Framework.Extensions.Color4Extensions; 8 | using osu.Framework.Graphics; 9 | using osu.Framework.Graphics.Primitives; 10 | using osu.Framework.Graphics.Rendering; 11 | using osu.Framework.Graphics.Rendering.Vertices; 12 | using osu.Framework.Graphics.Shaders; 13 | using osu.Framework.Graphics.Textures; 14 | using osuTK; 15 | using osuTK.Graphics; 16 | using Qsor.Game.Beatmaps; 17 | 18 | namespace Qsor.Game.Graphics.UserInterface.Screens.MainMenu.Drawables 19 | { 20 | public partial class DrawableLogoVisualisation : Drawable 21 | { 22 | private readonly IBindable _beatmap = new Bindable(); 23 | 24 | /// 25 | /// The number of bars to jump each update iteration. 26 | /// 27 | private const int IndexChange = 5; 28 | 29 | /// 30 | /// The maximum length of each bar in the visualiser. Will be reduced when kiai is not activated. 31 | /// 32 | private const float BarLength = 600; 33 | 34 | /// 35 | /// The number of bars in one rotation of the visualiser. 36 | /// 37 | private const int BarsPerVisualiser = 200; 38 | 39 | /// 40 | /// How many times we should stretch around the circumference (overlapping overselves). 41 | /// 42 | private const float VisualiserRounds = 5; 43 | 44 | /// 45 | /// How much should each bar go down each millisecond (based on a full bar). 46 | /// 47 | private const float DecayPerMillisecond = 0.0024f; 48 | 49 | /// 50 | /// Number of milliseconds between each amplitude update. 51 | /// 52 | private const float TimeBetweenUpdates = 50; 53 | 54 | /// 55 | /// The minimum amplitude to show a bar. 56 | /// 57 | private const float AmplitudeDeadZone = 1f / BarLength; 58 | 59 | private int _indexOffset; 60 | 61 | public Color4 AccentColour { get; set; } 62 | 63 | private readonly float[] _frequencyAmplitudes = new float[256]; 64 | 65 | private IShader _shader; 66 | private Texture _texture; 67 | 68 | public DrawableLogoVisualisation() 69 | { 70 | Blending = BlendingParameters.Additive; 71 | } 72 | 73 | [BackgroundDependencyLoader] 74 | private void Load(IRenderer renderer, ShaderManager shaders, BeatmapManager beatmapManager) 75 | { 76 | _texture = renderer.WhitePixel; 77 | _beatmap.BindTo(beatmapManager.WorkingBeatmap); 78 | _shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); 79 | 80 | UpdateColour(); 81 | } 82 | 83 | private void UpdateAmplitudes() 84 | { 85 | var track = _beatmap.Value?.Track?.IsLoaded == true ? _beatmap.Value.Track : null; 86 | var timingPoint = _beatmap.Value?.GetTimingPointAt(Time.Current); 87 | 88 | var temporalAmplitudes = track?.CurrentAmplitudes.FrequencyAmplitudes; 89 | 90 | for (var i = 0; i < BarsPerVisualiser; i++) 91 | { 92 | if (track?.IsRunning ?? false) 93 | { 94 | var targetAmplitude = (temporalAmplitudes?.Span[(i + _indexOffset) % BarsPerVisualiser] ?? 0) * (timingPoint?.KiaiMode == true ? 1 : 0.5f); 95 | if (targetAmplitude > _frequencyAmplitudes[i]) 96 | _frequencyAmplitudes[i] = targetAmplitude; 97 | } 98 | else 99 | { 100 | var index = (i + IndexChange) % BarsPerVisualiser; 101 | if (_frequencyAmplitudes[index] > _frequencyAmplitudes[i]) 102 | _frequencyAmplitudes[i] = _frequencyAmplitudes[index]; 103 | } 104 | } 105 | 106 | _indexOffset = (_indexOffset + IndexChange) % BarsPerVisualiser; 107 | } 108 | 109 | private void UpdateColour() 110 | { 111 | var defaultColour = Color4.White.Opacity(0.2f); 112 | 113 | AccentColour = defaultColour; 114 | } 115 | 116 | protected override void LoadComplete() 117 | { 118 | base.LoadComplete(); 119 | 120 | var delayed = Scheduler.AddDelayed(UpdateAmplitudes, TimeBetweenUpdates, true); 121 | delayed.PerformRepeatCatchUpExecutions = false; 122 | } 123 | 124 | protected override void Update() 125 | { 126 | base.Update(); 127 | 128 | var decayFactor = (float)Time.Elapsed * DecayPerMillisecond; 129 | 130 | for (var i = 0; i < BarsPerVisualiser; i++) 131 | { 132 | //3% of extra bar length to make it a little faster when bar is almost at it's minimum 133 | _frequencyAmplitudes[i] -= decayFactor * (_frequencyAmplitudes[i] + 0.03f); 134 | if (_frequencyAmplitudes[i] < 0) 135 | _frequencyAmplitudes[i] = 0; 136 | } 137 | 138 | Invalidate(Invalidation.DrawNode); 139 | } 140 | 141 | protected override DrawNode CreateDrawNode() => new VisualisationDrawNode(this); 142 | 143 | private class VisualisationDrawNode : DrawNode 144 | { 145 | protected new DrawableLogoVisualisation Source => (DrawableLogoVisualisation)base.Source; 146 | 147 | private IShader _shader; 148 | private Texture _texture; 149 | 150 | //Assuming the logo is a circle, we don't need a second dimension. 151 | private float _size; 152 | 153 | private Color4 _colour; 154 | private float[] _audioData; 155 | 156 | private IVertexBatch _vertexBatch; 157 | 158 | public VisualisationDrawNode(DrawableLogoVisualisation source) 159 | : base(source) 160 | { 161 | } 162 | 163 | public override void ApplyState() 164 | { 165 | base.ApplyState(); 166 | 167 | _shader = Source._shader; 168 | _texture = Source._texture; 169 | _size = Source.DrawSize.X; 170 | _colour = Source.AccentColour; 171 | _audioData = Source._frequencyAmplitudes; 172 | } 173 | 174 | protected override void Draw(IRenderer renderer) 175 | { 176 | base.Draw(renderer); 177 | 178 | _vertexBatch ??= renderer.CreateQuadBatch(100, 10); 179 | 180 | _shader.Bind(); 181 | 182 | var inflation = DrawInfo.MatrixInverse.ExtractScale().Xy; 183 | 184 | var colourInfo = DrawColourInfo.Colour; 185 | colourInfo.ApplyChild(_colour); 186 | 187 | if (_audioData != null) 188 | { 189 | for (var j = 0; j < VisualiserRounds; j++) 190 | { 191 | for (var i = 0; i < BarsPerVisualiser; i++) 192 | { 193 | if (_audioData[i] < AmplitudeDeadZone) 194 | continue; 195 | 196 | var rotation = float.DegreesToRadians(i / (float)BarsPerVisualiser * 360 + j * 360 / VisualiserRounds); 197 | var rotationCos = MathF.Cos(rotation); 198 | var rotationSin = MathF.Sin(rotation); 199 | //taking the cos and sin to the 0..1 range 200 | var barPosition = new Vector2(rotationCos / 2 + 0.5f, rotationSin / 2 + 0.5f) * _size; 201 | 202 | var barSize = new Vector2(_size * MathF.Sqrt(2 * (1 - MathF.Cos(float.DegreesToRadians(360f / BarsPerVisualiser)))) / 2f, BarLength * _audioData[i]); 203 | //The distance between the position and the sides of the bar. 204 | var bottomOffset = new Vector2(-rotationSin * barSize.X / 2, rotationCos * barSize.X / 2); 205 | //The distance between the bottom side of the bar and the top side. 206 | var amplitudeOffset = new Vector2(rotationCos * barSize.Y, rotationSin * barSize.Y); 207 | 208 | var rectangle = new Quad( 209 | Vector2Extensions.Transform(barPosition - bottomOffset, DrawInfo.Matrix), 210 | Vector2Extensions.Transform(barPosition - bottomOffset + amplitudeOffset, DrawInfo.Matrix), 211 | Vector2Extensions.Transform(barPosition + bottomOffset, DrawInfo.Matrix), 212 | Vector2Extensions.Transform(barPosition + bottomOffset + amplitudeOffset, DrawInfo.Matrix) 213 | ); 214 | 215 | renderer.DrawQuad( 216 | _texture, 217 | rectangle, 218 | colourInfo, 219 | null, 220 | _vertexBatch.AddAction, 221 | //barSize by itself will make it smooth more in the X axis than in the Y axis, this reverts that. 222 | Vector2.Divide(inflation, barSize.Yx)); 223 | } 224 | } 225 | } 226 | 227 | _shader.Unbind(); 228 | } 229 | 230 | protected override void Dispose(bool isDisposing) 231 | { 232 | base.Dispose(isDisposing); 233 | 234 | _vertexBatch.Dispose(); 235 | } 236 | } 237 | } 238 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Screens/MainMenu/Drawables/DrawableMenuSideFlashes.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. 2 | // See the LICENCE file in the repository root for full licence text. 3 | 4 | using osu.Framework.Allocation; 5 | using osu.Framework.Audio.Track; 6 | using osu.Framework.Bindables; 7 | using osu.Framework.Extensions.Color4Extensions; 8 | using osu.Framework.Graphics; 9 | using osu.Framework.Graphics.Colour; 10 | using osu.Framework.Graphics.Shapes; 11 | using osuTK.Graphics; 12 | using Qsor.Game.Beatmaps; 13 | using Qsor.Game.Graphics.Containers; 14 | 15 | namespace Qsor.Game.Graphics.UserInterface.Screens.MainMenu.Drawables 16 | { 17 | public partial class DrawableMenuSideFlashes : BeatSyncedContainer 18 | { 19 | private readonly IBindable _beatmap = new Bindable(); 20 | 21 | private Box _leftBox; 22 | private Box _rightBox; 23 | 24 | private const float AmplitudeDeadZone = 0.25f; 25 | private const float AlphaMultiplier = 1.0f; 26 | private const float KiaiMultiplier = 1.0f; 27 | 28 | private const int BoxMaxAlpha = 200; 29 | private const double BoxFadeInTime = 65; 30 | private const int BoxWidth = 200; 31 | 32 | public DrawableMenuSideFlashes() 33 | { 34 | EarlyActivationMilliseconds = BoxFadeInTime; 35 | 36 | RelativeSizeAxes = Axes.Both; 37 | Anchor = Anchor.Centre; 38 | Origin = Anchor.Centre; 39 | } 40 | 41 | [BackgroundDependencyLoader] 42 | private void Load(BeatmapManager beatmapManager) 43 | { 44 | _beatmap.BindTo(beatmapManager.WorkingBeatmap); 45 | 46 | Children = new Drawable[] 47 | { 48 | _leftBox = new Box 49 | { 50 | Anchor = Anchor.CentreLeft, 51 | Origin = Anchor.CentreLeft, 52 | RelativeSizeAxes = Axes.Y, 53 | Width = BoxWidth * 2, 54 | Height = 1.5f, 55 | // align off-screen to make sure our edges don't become visible during parallax. 56 | X = -BoxWidth, 57 | Alpha = 0, 58 | Blending = BlendingParameters.Additive 59 | }, 60 | _rightBox = new Box 61 | { 62 | Anchor = Anchor.CentreRight, 63 | Origin = Anchor.CentreRight, 64 | RelativeSizeAxes = Axes.Y, 65 | Width = BoxWidth * 2, 66 | Height = 1.5f, 67 | X = BoxWidth, 68 | Alpha = 0, 69 | Blending = BlendingParameters.Additive 70 | } 71 | }; 72 | 73 | var baseColour = Color4.White; 74 | var gradientDark = baseColour.Opacity(0).ToLinear(); 75 | var gradientLight = baseColour.Opacity(0.6f).ToLinear(); 76 | 77 | _leftBox.Colour = ColourInfo.GradientHorizontal(gradientLight, gradientDark); 78 | _rightBox.Colour = ColourInfo.GradientHorizontal(gradientDark, gradientLight); 79 | } 80 | 81 | protected override void OnNewBeat(int beatIndex, TimingPoint timingPoint, ChannelAmplitudes amplitudes) 82 | { 83 | var meter = timingPoint.Parent?.Meter ?? timingPoint.Meter; 84 | if (beatIndex < 0 || meter <= 0) 85 | return; 86 | 87 | if (timingPoint.KiaiMode ? beatIndex % 2 == 0 : beatIndex % meter == 0) 88 | Flash(_leftBox, timingPoint.MsPerBeat, timingPoint.KiaiMode, amplitudes); 89 | if (timingPoint.KiaiMode ? beatIndex % 2 == 1 : beatIndex % meter == 0) 90 | Flash(_rightBox, timingPoint.MsPerBeat, timingPoint.KiaiMode, amplitudes); 91 | } 92 | 93 | private void Flash(Drawable d, double beatLength, bool kiai, ChannelAmplitudes amplitudes) 94 | { 95 | d.FadeTo(kiai ? KiaiMultiplier : AlphaMultiplier, BoxFadeInTime) 96 | .Then() 97 | .FadeOut(beatLength, Easing.In); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Screens/MainMenu/Drawables/DrawableQsorLogo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. 2 | // See the LICENCE file in the repository root for full licence text. 3 | 4 | using System; 5 | using osu.Framework.Allocation; 6 | using osu.Framework.Audio.Track; 7 | using osu.Framework.Graphics; 8 | using osu.Framework.Graphics.Containers; 9 | using osu.Framework.Graphics.Sprites; 10 | using osu.Framework.Graphics.Textures; 11 | using osu.Framework.Input.Events; 12 | using Qsor.Game.Beatmaps; 13 | using Qsor.Game.Graphics.Containers; 14 | 15 | namespace Qsor.Game.Graphics.UserInterface.Screens.MainMenu.Drawables 16 | { 17 | public partial class DrawableQsorLogo : BeatSyncedContainer 18 | { 19 | private const double EarlyActivation = 60; 20 | 21 | private Container _logoHoverContainer; 22 | private Container _logoBeatContainer; 23 | private Sprite _qsorLogo; 24 | private Sprite _ripple; 25 | private Sprite _ghostingLogo; 26 | 27 | private DrawableLogoVisualisation _visualisation; 28 | private int _lastBeatIndex; 29 | 30 | [BackgroundDependencyLoader] 31 | private void Load(TextureStore ts) 32 | { 33 | AddInternal(new Container 34 | { 35 | Anchor = Anchor.Centre, 36 | Origin = Anchor.Centre, 37 | AutoSizeAxes = Axes.Both, 38 | 39 | Children = new [] 40 | { 41 | _logoHoverContainer = new CircularContainer 42 | { 43 | Anchor = Anchor.Centre, 44 | Origin = Anchor.Centre, 45 | AutoSizeAxes = Axes.Both, 46 | 47 | Children = new Drawable[] 48 | { 49 | _logoBeatContainer = new Container 50 | { 51 | Anchor = Anchor.Centre, 52 | Origin = Anchor.Centre, 53 | AutoSizeAxes = Axes.Both, 54 | 55 | Children = new Drawable[] 56 | { 57 | _ripple = new Sprite 58 | { 59 | Anchor = Anchor.Centre, 60 | Origin = Anchor.Centre, 61 | Texture = ts.Get("Logo-ghost"), 62 | }, 63 | _qsorLogo = new Sprite 64 | { 65 | Anchor = Anchor.Centre, 66 | Origin = Anchor.Centre, 67 | Texture = ts.Get("Logo"), 68 | }, 69 | _ghostingLogo = new Sprite 70 | { 71 | Anchor = Anchor.Centre, 72 | Origin = Anchor.Centre, 73 | Texture = ts.Get("Logo-ghost"), 74 | Alpha = .2f 75 | }, 76 | _visualisation = new DrawableLogoVisualisation 77 | { 78 | Anchor = Anchor.Centre, 79 | Origin = Anchor.Centre 80 | }, 81 | } 82 | } 83 | }, 84 | } 85 | } 86 | }); 87 | 88 | CornerRadius = Math.Min(_qsorLogo.Width, _qsorLogo.Height) / 2f; 89 | } 90 | 91 | protected override void Update() 92 | { 93 | base.Update(); 94 | 95 | const float scaleAdjustCutoff = 0.4f; 96 | 97 | if (!IsTrackPaused) 98 | { 99 | var maxAmplitude = _lastBeatIndex >= 0 ? Beatmap.Value.Track.CurrentAmplitudes.Maximum : 0; 100 | 101 | _qsorLogo.ScaleTo(1 - Math.Max(0, maxAmplitude - scaleAdjustCutoff) * 0.04f, 75, Easing.OutQuint); 102 | _ghostingLogo.ScaleTo(1 + Math.Max(0, maxAmplitude - scaleAdjustCutoff) * 0.04f, 75, Easing.OutQuint); 103 | } 104 | 105 | _visualisation.Scale = _qsorLogo.Scale; 106 | _visualisation.Size = _qsorLogo.Size; 107 | } 108 | 109 | protected override void OnNewBeat(int beatIndex, TimingPoint timingPoint, ChannelAmplitudes amplitudes) 110 | { 111 | _lastBeatIndex = beatIndex; 112 | 113 | var amplitudeAdjust = Math.Min(1, 0.4f + amplitudes.Maximum); 114 | 115 | _logoBeatContainer 116 | .ScaleTo(1 - 0.05f * amplitudeAdjust, EarlyActivation, Easing.Out) 117 | .Then() 118 | .ScaleTo(1, timingPoint.MsPerBeat * 2, Easing.OutQuint); 119 | 120 | _ghostingLogo 121 | .ScaleTo(1 + 0.05f * amplitudeAdjust, EarlyActivation, Easing.Out) 122 | .Then() 123 | .ScaleTo(1, timingPoint.MsPerBeat * 2, Easing.OutQuint); 124 | 125 | _ripple.ClearTransforms(); 126 | _ripple 127 | .ScaleTo(_qsorLogo.Scale) 128 | .ScaleTo(_qsorLogo.Scale * (1 + 0.16f * amplitudeAdjust), timingPoint.MsPerBeat, Easing.OutQuint) 129 | .FadeTo(0.3f * amplitudeAdjust) 130 | .FadeOut(timingPoint.MsPerBeat, Easing.OutQuint); 131 | 132 | if (timingPoint.KiaiMode) 133 | { 134 | _visualisation.ClearTransforms(); 135 | _visualisation 136 | .FadeTo(0.9f * amplitudeAdjust, EarlyActivation, Easing.Out) 137 | .Then() 138 | .FadeTo(0.5f, timingPoint.MsPerBeat); 139 | } 140 | } 141 | 142 | protected override bool OnHover(HoverEvent e) 143 | { 144 | _logoHoverContainer.ScaleTo(1.05f, 100, Easing.In); 145 | return true; 146 | } 147 | 148 | protected override void OnHoverLost(HoverLostEvent e) 149 | { 150 | _logoHoverContainer.ScaleTo(1, 100, Easing.Out); 151 | } 152 | } 153 | } -------------------------------------------------------------------------------- /Qsor.Game/Graphics/UserInterface/Screens/MainMenu/MainMenuScreen.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Audio; 3 | using osu.Framework.Bindables; 4 | using osu.Framework.Development; 5 | using osu.Framework.Graphics; 6 | using osu.Framework.Graphics.Containers; 7 | using osu.Framework.Input.Events; 8 | using osu.Framework.Localisation; 9 | using osu.Framework.Platform; 10 | using osu.Framework.Screens; 11 | using osu.Framework.Timing; 12 | using osuTK; 13 | using osuTK.Graphics; 14 | using Qsor.Game.Beatmaps; 15 | using Qsor.Game.Database; 16 | using Qsor.Game.Graphics.Containers; 17 | using Qsor.Game.Graphics.UserInterface.Overlays; 18 | using Qsor.Game.Graphics.UserInterface.Overlays.Notification; 19 | using Qsor.Game.Graphics.UserInterface.Screens.MainMenu.Containers; 20 | using Qsor.Game.Graphics.UserInterface.Screens.MainMenu.Drawables; 21 | using Qsor.Game.Updater; 22 | 23 | namespace Qsor.Game.Graphics.UserInterface.Screens.MainMenu 24 | { 25 | public partial class MainMenuScreen : Screen 26 | { 27 | private BackgroundImageContainer _background; 28 | private DrawableQsorLogo _drawableQsorLogo; 29 | private Toolbar _toolbar; 30 | private BottomBar _bottomBar; 31 | private DrawableMenuSideFlashes _sideFlashes; 32 | 33 | private Bindable _workingBeatmap = new(); 34 | 35 | [Resolved] 36 | private NotificationOverlay NotificationOverlay { get; set; } 37 | 38 | [Resolved] 39 | private Storage Storage { get; set; } 40 | 41 | [Resolved] 42 | private UpdateManager UpdateManager { get; set; } 43 | 44 | [Resolved] 45 | private GameHost Host { get; set; } 46 | 47 | [BackgroundDependencyLoader] 48 | private void Load(UpdaterOverlay updaterOverlay, AudioManager audioManager, QsorDbContextFactory ctxFactory, BeatmapManager beatmapManager) 49 | { 50 | var parallaxBack = new ParallaxContainer 51 | { 52 | ParallaxAmount = -0.005f 53 | }; 54 | parallaxBack._content.Add(_background = new BackgroundImageContainer 55 | { 56 | Anchor = Anchor.Centre, 57 | Origin = Anchor.Centre, 58 | FillMode = FillMode.Fill, 59 | }); 60 | 61 | _workingBeatmap.BindTo(beatmapManager.WorkingBeatmap); 62 | _workingBeatmap.ValueChanged += e => 63 | { 64 | if (e.NewValue == null) 65 | return; 66 | 67 | LoadComponent(e.NewValue); 68 | 69 | _background.SetTexture(e.NewValue.Background); 70 | audioManager.AddItem(e.NewValue.Track); 71 | }; 72 | 73 | var parallaxFront = new ParallaxContainer 74 | { 75 | ParallaxAmount = -0.015f, 76 | RelativeSizeAxes = Axes.Both, 77 | }; 78 | parallaxFront._content.Add(new DrawSizePreservingFillContainer 79 | { 80 | Child = _drawableQsorLogo = new DrawableQsorLogo 81 | { 82 | Anchor = Anchor.Centre, 83 | Origin = Anchor.Centre, 84 | AutoSizeAxes = Axes.Both, 85 | Scale = new Vector2(1f) * (DrawSize.X / DrawSize.Y) 86 | } 87 | }); 88 | 89 | InternalChildren = new Drawable[] 90 | { 91 | parallaxBack, 92 | parallaxFront, 93 | 94 | _sideFlashes = new DrawableMenuSideFlashes(), 95 | _toolbar = new Toolbar(), 96 | _bottomBar = new BottomBar(), 97 | 98 | updaterOverlay 99 | }; 100 | 101 | // Start a random map, TODO: remove this from here 102 | beatmapManager.NextRandomMap(); 103 | beatmapManager.WorkingBeatmap.Value?.Play(); 104 | } 105 | 106 | public override void OnEntering(ScreenTransitionEvent e) 107 | { 108 | _clock.Start(); 109 | 110 | this.FadeInFromZero(2500, Easing.InExpo).Finally(e => 111 | { 112 | NotificationOverlay.AddNotification(new LocalisableString( 113 | "Please note that this game is in very early development and is not representative of the final product. " + 114 | "If you encounter any issues, please report them on our GitHub repository!"), 115 | Color4.Orange, 10000); 116 | 117 | NotificationOverlay.AddNotification(new LocalisableString( 118 | "You can play different beatmaps by editing \"game.ini\" config file. " + 119 | "To open the Qsor configuration directory, click this notification!"), 120 | Color4.Orange, 10000, () => { Storage.OpenFileExternally(string.Empty); }); 121 | 122 | NotificationOverlay.AddNotification(new LocalisableString( 123 | $"You're currently running {QsorBaseGame.Version}! " + 124 | "Click here to view the changelog."), 125 | Color4.Gray, 10000, () => Host.OpenUrlExternally($"https://github.com/osuAkatsuki/Qsor/releases/tag/{QsorBaseGame.Version}")); 126 | 127 | if (!DebugUtils.IsDebugBuild) 128 | UpdateManager.CheckAvailable(); 129 | }); 130 | } 131 | 132 | public override bool OnExiting(ScreenExitEvent e) 133 | { 134 | this.FadeOutFromOne(2500, Easing.OutExpo); 135 | return true; 136 | } 137 | 138 | // Fade clock 139 | private StopwatchClock _clock = new(); 140 | private bool _isFading; 141 | 142 | protected override void Update() 143 | { 144 | if (_isFading || _clock.ElapsedMilliseconds <= 5000) 145 | return; 146 | 147 | _toolbar.FadeOut(8000); 148 | _bottomBar.FadeOut(8000); 149 | 150 | _isFading = true; 151 | } 152 | protected override bool OnMouseMove(MouseMoveEvent e) 153 | { 154 | if (_clock.ElapsedMilliseconds >= 5000) 155 | { 156 | _toolbar.ClearTransforms(); 157 | _bottomBar.ClearTransforms(); 158 | 159 | _toolbar.FadeIn(250); 160 | _bottomBar.FadeIn(250); 161 | 162 | _isFading = false; 163 | _clock.Restart(); 164 | } 165 | 166 | return true; 167 | } 168 | 169 | protected override bool OnClick(ClickEvent e) 170 | { 171 | if (_drawableQsorLogo.IsHovered) 172 | { 173 | NotificationOverlay.AddBigNotification("Gameplay as it stands right now is not implemented.\n" + 174 | "We want to implement the UI First before we do any gameplay features.\n" + 175 | "That way we can guarantee that it feels just right!", 10000); 176 | } 177 | 178 | return base.OnClick(e); 179 | } 180 | } 181 | } -------------------------------------------------------------------------------- /Qsor.Game/Input/KeyBindingInputHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Input; 5 | using osu.Framework.Input.Bindings; 6 | 7 | namespace Qsor.Game.Input 8 | { 9 | public partial class GlobalKeyBindingInputHandler : KeyBindingContainer, IHandleGlobalKeyboardInput 10 | { 11 | private readonly Drawable _handler; 12 | 13 | public GlobalKeyBindingInputHandler(QsorBaseGame game) 14 | : base(matchingMode: KeyCombinationMatchingMode.Modifiers) 15 | { 16 | _handler = game; 17 | } 18 | 19 | public override IEnumerable DefaultKeyBindings => new[] 20 | { 21 | new KeyBinding(new [] { InputKey.Control, InputKey.O }, GlobalAction.ToggleOptions), 22 | new KeyBinding(InputKey.Escape, GlobalAction.ExitOverlay), 23 | }; 24 | 25 | protected override IEnumerable KeyBindingInputQueue => 26 | _handler == null ? base.KeyBindingInputQueue : base.KeyBindingInputQueue.Prepend(_handler); 27 | } 28 | 29 | public enum GlobalAction 30 | { 31 | ToggleOptions, 32 | ExitOverlay 33 | } 34 | } -------------------------------------------------------------------------------- /Qsor.Game/Online/BanchoClient.cs: -------------------------------------------------------------------------------- 1 | namespace Qsor.Game.Online 2 | { 3 | public class BanchoClient 4 | { 5 | public const string WebUrl = "https://ripple.moe"; 6 | public const string AvatarUrl = "https://a.ripple.moe"; 7 | } 8 | } -------------------------------------------------------------------------------- /Qsor.Game/Online/BeatmapMirrorAccess.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using osu.Framework.Allocation; 3 | using osu.Framework.IO.Network; 4 | using osu.Framework.Platform; 5 | 6 | namespace Qsor.Game.Online 7 | { 8 | public partial class BeatmapMirrorAccess : IDependencyInjectionCandidate 9 | { 10 | private const string BeatmapMirror = "https://api.nerinyan.moe"; 11 | 12 | [Resolved] 13 | private Storage Storage { get; set; } 14 | 15 | /// 16 | /// Download a beatmap from a given 17 | /// 18 | /// Set ID 19 | public void DownloadBeatmap(int setId) 20 | { 21 | using var fileReq = new FileWebRequest(Storage.GetFullPath($"Songs/{setId}.osz"), $"{BeatmapMirror}/d/{setId}"); 22 | 23 | fileReq.Perform(); // Works 24 | } 25 | 26 | /// 27 | /// Download a beatmap from a given asynchronously 28 | /// 29 | /// Set ID 30 | /// 31 | /// NOTE: this seems broken right now. 32 | /// 33 | public async Task DownloadBeatmapAsync(int setId) 34 | { 35 | using var fileReq = new FileWebRequest(Storage.GetFullPath($"Songs/{setId}.osz"), $"{BeatmapMirror}/d/{setId}"); 36 | 37 | await fileReq.PerformAsync(); // Gets stuck 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Qsor.Game/Online/UserManager.cs: -------------------------------------------------------------------------------- 1 | using Qsor.Game.Online.Users; 2 | 3 | namespace Qsor.Game.Online 4 | { 5 | public class UserManager 6 | { 7 | public User User { get; } = new() 8 | { 9 | Id = 0, 10 | Username = "Guest" 11 | }; 12 | } 13 | } -------------------------------------------------------------------------------- /Qsor.Game/Online/Users/User.cs: -------------------------------------------------------------------------------- 1 | namespace Qsor.Game.Online.Users 2 | { 3 | public class User 4 | { 5 | public int Id = 1; 6 | public string Username = "Guest"; 7 | 8 | public UserStatistics Statistics { get; } = new(); 9 | } 10 | } -------------------------------------------------------------------------------- /Qsor.Game/Online/Users/UserStatistics.cs: -------------------------------------------------------------------------------- 1 | namespace Qsor.Game.Online.Users 2 | { 3 | public class UserStatistics 4 | { 5 | public int PerformancePoints = 0; 6 | public double Accuracy = 0; 7 | 8 | public int Rank = 0; 9 | 10 | public int GetLevel() => 0; 11 | public double GetProgress() => .5d; 12 | } 13 | } -------------------------------------------------------------------------------- /Qsor.Game/Qsor.Game.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Qsor.Game 5 | Qsor.Game 6 | 9 7 | net8.0 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Qsor.Game/QsorBaseGame.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using osu.Framework.Allocation; 4 | using osu.Framework.Development; 5 | using osu.Framework.Graphics; 6 | using osu.Framework.Graphics.Cursor; 7 | using osu.Framework.IO.Stores; 8 | using osu.Framework.Platform; 9 | using osu.Framework.Screens; 10 | using Qsor.Game.Beatmaps; 11 | using Qsor.Game.Configuration; 12 | using Qsor.Game.Database; 13 | using Qsor.Game.Graphics.Containers; 14 | using Qsor.Game.Graphics.UserInterface.Overlays; 15 | using Qsor.Game.Graphics.UserInterface.Overlays.Notification; 16 | using Qsor.Game.Graphics.UserInterface.Overlays.Settings; 17 | using Qsor.Game.Online; 18 | using Qsor.Game.Updater; 19 | using Qsor.Game.Utility; 20 | 21 | namespace Qsor.Game 22 | { 23 | public partial class QsorBaseGame : osu.Framework.Game 24 | { 25 | protected readonly string[] Args; 26 | protected BeatmapManager BeatmapManager; 27 | protected UserManager UserManager; 28 | 29 | protected BeatmapMirrorAccess BeatmapMirrorAccess; 30 | 31 | protected QsorDbContextFactory QsorDbContextFactory; 32 | protected QsorConfigManager ConfigManager; 33 | 34 | protected NotificationOverlay NotificationOverlay; 35 | protected UpdaterOverlay UpdaterOverlay; 36 | protected SentryLogger SentryLogger; 37 | protected SettingsOverlay SettingsOverlay; 38 | protected TooltipContainer TooltipContainer; 39 | 40 | public UpdateManager UpdateManager; 41 | 42 | private DependencyContainer _dependencies; 43 | 44 | private ScreenStack _stack; 45 | 46 | public static string Version => !DebugUtils.IsDebugBuild 47 | ? Assembly.GetEntryAssembly()?.GetName().Version?.ToString(3) 48 | : DateTime.Now.ToString("yyyy.Mdd.0") + (DebugUtils.IsDebugBuild ? " Debug Build" : string.Empty); 49 | 50 | protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => 51 | _dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); 52 | 53 | public QsorBaseGame(string[] args) 54 | { 55 | Args = args; 56 | } 57 | 58 | [BackgroundDependencyLoader] 59 | private void Load(Storage storage) 60 | { 61 | // Load important dependencies 62 | { 63 | _dependencies.Cache(UserManager = new UserManager()); 64 | 65 | _dependencies.Cache(BeatmapMirrorAccess = new BeatmapMirrorAccess()); 66 | Dependencies.Inject(BeatmapMirrorAccess); 67 | 68 | _dependencies.Cache(QsorDbContextFactory = new QsorDbContextFactory(storage)); 69 | _dependencies.Cache(ConfigManager = new QsorConfigManager(storage)); 70 | 71 | _dependencies.Cache(NotificationOverlay = new NotificationOverlay()); 72 | 73 | _dependencies.Cache(SentryLogger = new SentryLogger(this)); 74 | 75 | _dependencies.CacheAs(this); 76 | _dependencies.CacheAs(Host); 77 | 78 | _dependencies.Cache(BeatmapManager = new BeatmapManager()); 79 | Dependencies.Inject(BeatmapManager); 80 | 81 | UpdateManager ??= CreateUpdater(); 82 | UpdaterOverlay = new UpdaterOverlay(); 83 | 84 | _dependencies.Cache(UpdaterOverlay); 85 | _dependencies.CacheAs(UpdateManager); 86 | 87 | LoadComponent(UpdateManager); 88 | } 89 | 90 | // Add our Resources/ directory 91 | Resources.AddStore(new NamespacedResourceStore(new DllResourceStore(typeof(QsorGame).Assembly), @"Resources")); 92 | 93 | // Migrate the given database 94 | QsorDbContextFactory.Get().Migrate(); 95 | 96 | // Root container for everything and anything, we have QsorToolTipContainer as root 97 | // so we can display our tooltips cleanly. 98 | Add(TooltipContainer = new QsorTooltipContainer(null) 99 | { 100 | RelativeSizeAxes = Axes.Both, 101 | }); 102 | 103 | // Update config (if necessary) 104 | ConfigManager.Save(); 105 | 106 | _stack = new ScreenStack(); 107 | 108 | TooltipContainer.Add(_stack); 109 | TooltipContainer.Add(SettingsOverlay = new SettingsOverlay()); 110 | TooltipContainer.Add(NotificationOverlay); 111 | } 112 | 113 | protected virtual UpdateManager CreateUpdater() => new DummyUpdater(); 114 | 115 | protected override void Dispose(bool isDisposing) 116 | { 117 | if (isDisposing) { 118 | ConfigManager?.Dispose(); 119 | NotificationOverlay?.Dispose(); 120 | 121 | SentryLogger?.Dispose(); 122 | } 123 | 124 | base.Dispose(isDisposing); 125 | } 126 | 127 | public void PushScreen(Screen screen) 128 | { 129 | _stack.Push(screen); 130 | } 131 | 132 | public void ExitScreen() 133 | { 134 | _stack.Exit(); 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /Qsor.Game/QsorGame.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using osu.Framework.Allocation; 3 | using osu.Framework.Development; 4 | using osu.Framework.Input.Bindings; 5 | using osu.Framework.Input.Events; 6 | using osuTK.Input; 7 | using Qsor.Game.Graphics.UserInterface.Screens; 8 | using Qsor.Game.Graphics.UserInterface.Screens.MainMenu; 9 | using Qsor.Game.Input; 10 | 11 | namespace Qsor.Game 12 | { 13 | [Cached] 14 | public partial class QsorGame : QsorBaseGame, IKeyBindingHandler 15 | { 16 | private GlobalKeyBindingInputHandler _keyBindingInputHandler; 17 | 18 | [BackgroundDependencyLoader] 19 | private void Load() 20 | { 21 | Audio.Frequency.Set(1); 22 | Audio.Volume.Set(.35); 23 | 24 | Window.Title = $"Qsor - {Version}"; 25 | 26 | Add(_keyBindingInputHandler = new GlobalKeyBindingInputHandler(this)); 27 | 28 | if (!DebugUtils.IsDebugBuild) 29 | { 30 | PushScreen(new IntroScreen()); 31 | 32 | Scheduler.AddDelayed(() => 33 | { 34 | ExitScreen(); 35 | 36 | Scheduler.AddDelayed(() => PushScreen(new MainMenuScreen()), 2000); 37 | }, 6000); 38 | } 39 | else 40 | { 41 | PushScreen(new MainMenuScreen()); 42 | } 43 | } 44 | 45 | protected override bool OnKeyDown(KeyDownEvent e) 46 | { 47 | switch (e.Key) 48 | { 49 | case Key.Down: 50 | Audio.Frequency.Value -= .1; 51 | return true; 52 | case Key.Up: 53 | Audio.Frequency.Value += .1; 54 | return true; 55 | case Key.Space: 56 | if (!BeatmapManager.WorkingBeatmap.Value.Track.IsRunning) 57 | BeatmapManager.WorkingBeatmap.Value.Track.Start(); 58 | else 59 | BeatmapManager.WorkingBeatmap.Value.Track.Stop(); 60 | return true; 61 | default: 62 | return false; 63 | } 64 | } 65 | 66 | public QsorGame(string[] args) : base(args) 67 | { 68 | } 69 | 70 | public bool OnPressed(KeyBindingPressEvent e) 71 | { 72 | switch (e.Action) 73 | { 74 | case GlobalAction.ToggleOptions: 75 | if (SettingsOverlay.IsShown) 76 | SettingsOverlay.Hide(); 77 | else 78 | SettingsOverlay.Show(); 79 | break; 80 | 81 | case GlobalAction.ExitOverlay: 82 | if (SettingsOverlay.IsShown) 83 | SettingsOverlay.Hide(); 84 | break; 85 | 86 | default: 87 | throw new ArgumentOutOfRangeException(nameof(e.Action), e.Action, null); 88 | } 89 | 90 | return true; 91 | } 92 | 93 | public void OnReleased(KeyBindingReleaseEvent e) 94 | { 95 | } 96 | 97 | protected override bool OnClick(ClickEvent e) 98 | { 99 | if (SettingsOverlay.IsShown && !SettingsOverlay.IsHovered) 100 | SettingsOverlay.Hide(); 101 | 102 | return base.OnClick(e); 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /Qsor.Game/Resources/Textures/Logo-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mempler/Qsor/c1124f16faca2d46c642be57651690558be1e599/Qsor.Game/Resources/Textures/Logo-256x256.png -------------------------------------------------------------------------------- /Qsor.Game/Resources/Textures/Logo-ghost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mempler/Qsor/c1124f16faca2d46c642be57651690558be1e599/Qsor.Game/Resources/Textures/Logo-ghost.png -------------------------------------------------------------------------------- /Qsor.Game/Resources/Textures/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mempler/Qsor/c1124f16faca2d46c642be57651690558be1e599/Qsor.Game/Resources/Textures/Logo.png -------------------------------------------------------------------------------- /Qsor.Game/Resources/Textures/approachcircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mempler/Qsor/c1124f16faca2d46c642be57651690558be1e599/Qsor.Game/Resources/Textures/approachcircle.png -------------------------------------------------------------------------------- /Qsor.Game/Resources/Textures/hitcircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mempler/Qsor/c1124f16faca2d46c642be57651690558be1e599/Qsor.Game/Resources/Textures/hitcircle.png -------------------------------------------------------------------------------- /Qsor.Game/Resources/Textures/hitcircleoverlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mempler/Qsor/c1124f16faca2d46c642be57651690558be1e599/Qsor.Game/Resources/Textures/hitcircleoverlay.png -------------------------------------------------------------------------------- /Qsor.Game/Resources/Textures/slider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mempler/Qsor/c1124f16faca2d46c642be57651690558be1e599/Qsor.Game/Resources/Textures/slider.png -------------------------------------------------------------------------------- /Qsor.Game/Resources/Textures/sliderb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mempler/Qsor/c1124f16faca2d46c642be57651690558be1e599/Qsor.Game/Resources/Textures/sliderb.png -------------------------------------------------------------------------------- /Qsor.Game/Updater/DummyUpdateManager.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | 3 | namespace Qsor.Game.Updater 4 | { 5 | [Cached] 6 | public partial class DummyUpdater : UpdateManager 7 | { 8 | public override void CheckAvailable() 9 | { 10 | BindableStatus.Value = UpdaterStatus.Latest; 11 | } 12 | 13 | public override void UpdateGame() 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Qsor.Game/Updater/UpdateManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using osu.Framework.Allocation; 3 | using osu.Framework.Bindables; 4 | using osu.Framework.Graphics; 5 | using osu.Framework.Localisation; 6 | using Qsor.Game.Graphics.UserInterface.Overlays; 7 | 8 | namespace Qsor.Game.Updater 9 | { 10 | [Cached] 11 | public abstract partial class UpdateManager : Component 12 | { 13 | public readonly Bindable BindableStatus = new(); 14 | public readonly BindableFloat BindableProgress = new(); 15 | 16 | public abstract void CheckAvailable(); 17 | public abstract void UpdateGame(); 18 | 19 | [Resolved] 20 | private UpdaterOverlay UpdaterOverlay { get; set; } 21 | 22 | [BackgroundDependencyLoader] 23 | private void Load() 24 | { 25 | BindableStatus.ValueChanged += e => 26 | { 27 | switch (e.NewValue) 28 | { 29 | case UpdaterStatus.Latest: 30 | break; // Ignore 31 | 32 | case UpdaterStatus.Pending: 33 | case UpdaterStatus.Downloading: 34 | UpdaterOverlay.Show(); 35 | break; 36 | 37 | case UpdaterStatus.Ready: 38 | UpdaterOverlay.Text = new LocalisableString("Click here to restart!"); 39 | UpdaterOverlay.Show(); 40 | break; 41 | default: 42 | throw new ArgumentOutOfRangeException(); 43 | } 44 | }; 45 | 46 | BindableProgress.ValueChanged += e => 47 | UpdaterOverlay.Text = new LocalisableString($"Updating Qsor... {e.NewValue}%"); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /Qsor.Game/Updater/UpdaterStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Qsor.Game.Updater 2 | { 3 | public enum UpdaterStatus 4 | { 5 | Latest, 6 | Pending, 7 | Downloading, 8 | Ready 9 | } 10 | } -------------------------------------------------------------------------------- /Qsor.Game/Utility/SentryLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using osu.Framework.Development; 3 | using osu.Framework.Logging; 4 | using Sentry; 5 | 6 | namespace Qsor.Game.Utility 7 | { 8 | public class SentryLogger : IDisposable 9 | { 10 | private IDisposable _sentry; 11 | 12 | public SentryLogger(QsorBaseGame game) 13 | { 14 | if (DebugUtils.IsDebugBuild) 15 | return; 16 | 17 | _sentry = SentrySdk.Init(o => 18 | { 19 | o.Dsn = "https://ddefbeb05e074f9c993bf1a72eb2a602@o169266.ingest.sentry.io/5193034"; 20 | o.Release = QsorBaseGame.Version; 21 | }); 22 | 23 | Exception lastException = null; 24 | Logger.NewEntry += entry => 25 | { 26 | if (entry.Level < LogLevel.Verbose) 27 | return; 28 | 29 | var exception = entry.Exception; 30 | 31 | if (exception != null) 32 | { 33 | if (lastException != null && // We shouldn't resubmit the same exception 34 | lastException.Message == exception.Message && 35 | exception.StackTrace?.StartsWith(lastException.StackTrace ?? string.Empty) == true) 36 | return; 37 | 38 | SentrySdk.CaptureEvent(new SentryEvent(exception) { Message = entry.Message }); 39 | lastException = exception; 40 | } 41 | else 42 | { 43 | SentrySdk.AddBreadcrumb(entry.Message, entry.Target.ToString(), "qsor-logger"); 44 | } 45 | }; 46 | } 47 | 48 | public void Dispose() 49 | { 50 | _sentry?.Dispose(); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /Qsor.NativeLibs/Qsor.NativeLibs.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Library 4 | net8.0 5 | true 6 | 7 | 8 | 9 | true 10 | Qsor Native Libraries 11 | Native libraries for Qsor 12 | Qsor.NativeLibs 13 | qsor libraries 14 | false 15 | 16 | 17 | 18 | 19 | true 20 | runtimes 21 | 22 | 23 | 24 | 25 | true 26 | lib\$(TargetFramework) 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Qsor.NativeLibs/_._: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /Qsor.Tests/Program.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework; 2 | 3 | namespace Qsor.Tests 4 | { 5 | internal static class Program 6 | { 7 | private static void Main(string[] args) 8 | { 9 | using var host = Host.GetSuitableDesktopHost(@"Qsor"); 10 | host.Run(new QsorTestGame(args)); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /Qsor.Tests/Qsor.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 9 5 | Exe 6 | net8.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Qsor.Tests/QsorTestGame.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.IO.Stores; 3 | using osu.Framework.Testing; 4 | using Qsor.Game; 5 | 6 | namespace Qsor.Tests 7 | { 8 | public partial class QsorTestGame : QsorBaseGame 9 | { 10 | [BackgroundDependencyLoader] 11 | private void Load() 12 | { 13 | Resources.AddStore(new NamespacedResourceStore(new DllResourceStore(typeof(QsorTestGame).Assembly), @"Resources")); 14 | 15 | Add(new TestBrowser("Qsor.Tests")); 16 | } 17 | 18 | public QsorTestGame(string[] args) : base(args) 19 | { 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Qsor.Tests/Visual/Overlays/TestSceneMusicPlayerOverlay.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Shapes; 4 | using osu.Framework.Testing; 5 | using osuTK.Graphics; 6 | using Qsor.Game.Graphics.UserInterface.Overlays; 7 | 8 | namespace Qsor.Tests.Visual.Overlays 9 | { 10 | public partial class TestSceneMusicPlayerOverlay : TestScene 11 | { 12 | private MusicPlayerOverlay _musicPlayer; 13 | 14 | [SetUpSteps] 15 | public void Setup() 16 | { 17 | // Background 18 | Add(new Box 19 | { 20 | Colour = new Color4(0.2f, 0.2f, 0.2f, 1.0f), 21 | RelativeSizeAxes = Axes.Both, 22 | }); 23 | 24 | Add(_musicPlayer = new MusicPlayerOverlay()); 25 | } 26 | 27 | [BackgroundDependencyLoader] 28 | private void Load() 29 | { 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /Qsor.Tests/Visual/Overlays/TestSceneNotificationOverlay.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NLipsum.Core; 3 | using osu.Framework.Allocation; 4 | using osu.Framework.Graphics; 5 | using osu.Framework.Graphics.Shapes; 6 | using osu.Framework.Localisation; 7 | using osu.Framework.Testing; 8 | using osuTK.Graphics; 9 | using Qsor.Game.Graphics.UserInterface.Overlays.Notification; 10 | 11 | namespace Qsor.Tests.Visual.Overlays 12 | { 13 | public partial class TestSceneNotificationOverlay : TestScene 14 | { 15 | private LipsumGenerator _lipsumGenerator; 16 | private NotificationOverlay _notificationOverlay; 17 | 18 | [SetUpSteps] 19 | public void Setup() 20 | { 21 | // Background 22 | Add(new Box 23 | { 24 | Colour = new Color4(0.2f, 0.2f, 0.2f, 1.0f), 25 | RelativeSizeAxes = Axes.Both, 26 | }); 27 | 28 | _lipsumGenerator = new LipsumGenerator(); 29 | _notificationOverlay = new NotificationOverlay(); 30 | 31 | Add(_notificationOverlay = new NotificationOverlay()); 32 | } 33 | 34 | [BackgroundDependencyLoader] 35 | private void Load() 36 | { 37 | AddStep("Create random Lorem Ipsum", () => 38 | { 39 | var random = new Random(); 40 | var randomColour = new Color4((float) random.NextDouble(), (float) random.NextDouble(), (float) random.NextDouble(), 1f); 41 | 42 | _notificationOverlay.AddNotification( 43 | new LocalisableString(_lipsumGenerator.GenerateLipsum(4, Features.Sentences, FormatStrings.Paragraph.LineBreaks) 44 | .Trim()), 45 | randomColour, 5000); 46 | }); 47 | 48 | AddStep("Create Big Notification", () => 49 | { 50 | _notificationOverlay.AddBigNotification( 51 | new LocalisableString(_lipsumGenerator.GenerateLipsum(4, Features.Sentences, FormatStrings.Paragraph.LineBreaks) 52 | .Trim()), 53 | 5000); 54 | }); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /Qsor.Tests/Visual/Overlays/TestSceneSettingsOverlay.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics.Textures; 3 | using osu.Framework.Testing; 4 | using Qsor.Game.Graphics.Containers; 5 | using Qsor.Game.Graphics.UserInterface.Overlays.Settings; 6 | 7 | namespace Qsor.Tests.Visual.Overlays 8 | { 9 | public partial class TestSceneSettingsOverlay : TestScene 10 | { 11 | [BackgroundDependencyLoader] 12 | private void Load(TextureStore ts) 13 | { 14 | var bg = new BackgroundImageContainer(); 15 | Add(bg); 16 | bg.SetTexture(ts.Get("https://3.bp.blogspot.com/-906HDJiF4Nk/UbAN4_DrK_I/AAAAAAAAAvE/pZQwo-u2RbQ/s1600/Background+images.jpg")); 17 | 18 | SettingsOverlay settingsOverlay; 19 | Add(settingsOverlay = new SettingsOverlay()); 20 | 21 | AddStep("Show Overlay", settingsOverlay.Show); 22 | AddStep("Hide Overlay", settingsOverlay.Hide); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /Qsor.Tests/Visual/Overlays/TestSceneUpdaterOverlay.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using osu.Framework.Allocation; 3 | using osu.Framework.Testing; 4 | using Qsor.Game.Graphics.UserInterface.Overlays; 5 | 6 | namespace Qsor.Tests.Visual.Overlays 7 | { 8 | public partial class TestSceneUpdaterOverlay : TestScene 9 | { 10 | [BackgroundDependencyLoader] 11 | private void Load() 12 | { 13 | Console.WriteLine("Test"); 14 | 15 | var updaterOverlay = new UpdaterOverlay(); 16 | 17 | Add(updaterOverlay); 18 | 19 | AddStep("Show", updaterOverlay.Show); 20 | AddStep("Hide", updaterOverlay.Hide); 21 | } 22 | 23 | } 24 | } -------------------------------------------------------------------------------- /Qsor.Tests/Visual/Overlays/TestSceneUserOverlay.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Testing; 3 | using Qsor.Game.Graphics.UserInterface.Overlays; 4 | 5 | namespace Qsor.Tests.Visual.Overlays 6 | { 7 | public partial class TestSceneUserOverlay : TestScene 8 | { 9 | [BackgroundDependencyLoader] 10 | private void Load() 11 | { 12 | AddStep("Setup", () => Add(new UserOverlay())); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Qsor.Tests/Visual/Scenes/IntroTestScene.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using osu.Framework.Allocation; 3 | using osu.Framework.Screens; 4 | using osu.Framework.Testing; 5 | using Qsor.Game.Graphics.UserInterface.Screens; 6 | 7 | namespace Qsor.Tests.Visual.Scenes 8 | { 9 | [TestFixture] 10 | public partial class IntroTestScene : TestScene 11 | { 12 | [BackgroundDependencyLoader] 13 | private void Load() 14 | { 15 | var stack = new ScreenStack(); 16 | 17 | Add(stack); 18 | 19 | AddStep("Start sequence", () => 20 | { 21 | stack.Push(new IntroScreen()); 22 | }); 23 | 24 | AddStep("Exit sequence", () => 25 | { 26 | stack.Exit(); 27 | }); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Qsor.Tests/Visual/TestSceneQsorGame.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Testing; 3 | using Qsor.Game; 4 | 5 | namespace Qsor.Tests.Visual 6 | { 7 | public partial class TestSceneQsorGame : TestScene 8 | { 9 | [BackgroundDependencyLoader] 10 | private void Load() 11 | { 12 | AddGame(new QsorGame(new []{ string.Empty })); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Qsor.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Qsor.Game", "Qsor.Game\Qsor.Game.csproj", "{80063128-CB8C-4F95-9EE3-C4539D01D580}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Qsor.Tests", "Qsor.Tests\Qsor.Tests.csproj", "{2DC454D9-325D-4FA4-BEE2-D4FC3A1EAD48}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Qsor.Desktop", "Qsor.Desktop\Qsor.Desktop.csproj", "{74EC9492-F46A-48A7-858F-170090C7BF35}" 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Qsor.Deploy", "Qsor.Deploy\Qsor.Deploy.csproj", "{2D2B2A6E-B641-46E5-97EE-02B9172C9494}" 10 | EndProject 11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Qsor.NativeLibs", "Qsor.NativeLibs\Qsor.NativeLibs.csproj", "{BB29328A-2A1A-440A-8B50-AA0C7E2E2BC2}" 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Release|Any CPU = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {80063128-CB8C-4F95-9EE3-C4539D01D580}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {80063128-CB8C-4F95-9EE3-C4539D01D580}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {80063128-CB8C-4F95-9EE3-C4539D01D580}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {80063128-CB8C-4F95-9EE3-C4539D01D580}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {2DC454D9-325D-4FA4-BEE2-D4FC3A1EAD48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {2DC454D9-325D-4FA4-BEE2-D4FC3A1EAD48}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {2DC454D9-325D-4FA4-BEE2-D4FC3A1EAD48}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {2DC454D9-325D-4FA4-BEE2-D4FC3A1EAD48}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {74EC9492-F46A-48A7-858F-170090C7BF35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {74EC9492-F46A-48A7-858F-170090C7BF35}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {74EC9492-F46A-48A7-858F-170090C7BF35}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {74EC9492-F46A-48A7-858F-170090C7BF35}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {2D2B2A6E-B641-46E5-97EE-02B9172C9494}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {2D2B2A6E-B641-46E5-97EE-02B9172C9494}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {2D2B2A6E-B641-46E5-97EE-02B9172C9494}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {2D2B2A6E-B641-46E5-97EE-02B9172C9494}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {BB29328A-2A1A-440A-8B50-AA0C7E2E2BC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {BB29328A-2A1A-440A-8B50-AA0C7E2E2BC2}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {BB29328A-2A1A-440A-8B50-AA0C7E2E2BC2}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {BB29328A-2A1A-440A-8B50-AA0C7E2E2BC2}.Release|Any CPU.Build.0 = Release|Any CPU 39 | EndGlobalSection 40 | EndGlobal 41 | -------------------------------------------------------------------------------- /Qsor.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True 4 | True 5 | True -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | ## Qsor 6 | 7 | [![Official Discord](https://discordapp.com/api/guilds/1237661234073960531/widget.png?style=shield)](https://discord.gg/ujNk7sVQCt) [![CodeFactor](https://www.codefactor.io/repository/github/mempler/qsor/badge)](https://www.codefactor.io/repository/github/mempler/qsor) [![Build status](https://ci.appveyor.com/api/projects/status/0jec4r3nxqa6nq8g?svg=true)](https://ci.appveyor.com/project/mempler/qsor) 8 | 9 | for people who wan't the nostalgic feeling of osu!Stable once osu!Lazer gets released. 10 | 11 | ## Requirements 12 | 13 | #### Knowledge 14 | 15 | you'll need prior knowledge in C\# and .NET 5 and osu!Framework if you want to develop on Qsor! 16 | 17 | #### Dependencies 18 | 19 | * [.NET 5](https://dotnet.microsoft.com) 20 | 21 | ## Setup Qsor 22 | 23 | If you're a not a developer, you can simply go over to the [releases](https://github.com/mempler/qsor/releases) and grab the latest release. 24 | 25 | but if you're one, there you go: 26 | 27 | ```shell 28 | :~$ git clone https://github.com/mempler/qsor.git 29 | :~$ cd qsor 30 | :~$ dotnet run Qsor.Desktop -c Release 31 | ``` 32 | 33 | ## License 34 | 35 | Qsor's code is licensed under the [MIT licence](https://opensource.org/licenses/MIT). Please see [the licence file](./LICENSE) for more information. [tl;dr](https://tldrlegal.com/license/mit-license) you can do whatever you want as long as you include the original copyright and license notice in any copy of the software/source. 36 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | image: Visual Studio 2022 3 | configuration: Release 4 | init: 5 | - ps: Update-AppveyorBuild -Version "$(Get-Date -format yyyy.Mdd).$env:appveyor_build_number" 6 | dotnet_csproj: 7 | patch: true 8 | file: '**\*.csproj' 9 | version: '{version}' 10 | version_prefix: '{version}' 11 | package_version: '{version}' 12 | assembly_version: '{version}' 13 | file_version: '{version}' 14 | informational_version: '{version}' 15 | before_build: 16 | - ps: dotnet restore 17 | build: 18 | project: Qsor.Desktop 19 | parallel: true 20 | verbosity: minimal 21 | deploy: off 22 | on_success: 23 | - ps: >- 24 | Invoke-RestMethod https://raw.githubusercontent.com/DiscordHooks/appveyor-discord-webhook/master/send.ps1 -o send.ps1 25 | 26 | ./send.ps1 success $env:WEBHOOK_URL 27 | on_failure: 28 | - sh: >- 29 | Invoke-RestMethod https://raw.githubusercontent.com/DiscordHooks/appveyor-discord-webhook/master/send.ps1 -o send.ps1 30 | 31 | ./send.ps1 failure $env:WEBHOOK_URL 32 | -------------------------------------------------------------------------------- /generate_changelog.ps1: -------------------------------------------------------------------------------- 1 | $TYPE = "DEV" 2 | $LAST_TAG = git describe --tags --abbrev=0 3 | $COMMIT_COUNT = git rev-list "$LAST_TAG..HEAD" --count 4 | $DATE = Get-Date -Format "yyyy.ddMM" 5 | $CHANGE_LOG = git log "$LAST_TAG..HEAD" --pretty=format:"%h %s" 6 | 7 | Write-Output "Create Tag $TYPE-$DATE.$COMMIT_COUNT" 8 | Write-Output "Changelog:" 9 | Write-Output $CHANGE_LOG 10 | 11 | git tag "$TYPE-$DATE.$COMMIT_COUNT" -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "5.0", 4 | "rollForward": "latestMajor", 5 | "allowPrerelease": false 6 | } 7 | } --------------------------------------------------------------------------------