├── .gitattributes ├── .gitignore ├── LICENSE ├── LocalizationGenerator ├── LocalisationGenerator.csproj ├── Program.cs └── Source │ ├── en.jsonc │ ├── es.jsonc │ ├── meta.jsonc │ ├── pl.jsonc │ └── ru.jsonc ├── README.md ├── osu.Game.Rulesets.RurusettoAddon.Tests ├── TestSceneOsuGame.cs ├── TestSceneOverlay.cs ├── VisualTestRunner.cs └── osu.Game.Rulesets.RurusettoAddon.Tests.csproj ├── osu.Game.Rulesets.RurusettoAddon.sln ├── osu.Game.Rulesets.RurusettoAddon.sln.DotSettings ├── osu.Game.Rulesets.RurusettoAddon ├── API │ ├── APIRuleset.cs │ ├── APIUser.cs │ ├── BeatmapRecommendation.cs │ ├── ListingEntry.cs │ ├── RulesetDetail.cs │ ├── RurusettoAPI.cs │ ├── Status.cs │ ├── Subpage.cs │ ├── SubpageListingEntry.cs │ ├── UserDetail.cs │ └── UserProfile.cs ├── APIRulesetStore.cs ├── APIUserStore.cs ├── Configuration │ └── RurusettoConfigManager.cs ├── Extensions.cs ├── Localisation │ ├── Strings.cs │ ├── Strings.es.resx │ ├── Strings.pl.resx │ ├── Strings.resx │ └── Strings.ru.resx ├── Resources │ └── Textures │ │ ├── cover.jpg │ │ ├── default_pfp.png │ │ ├── default_wiki_cover.jpg │ │ ├── oh_no.png │ │ └── rurusetto-logo.png ├── RulesetDownloader.cs ├── RurusettoAddonRuleset.cs ├── RurusettoIcon.cs ├── TextureNames.cs ├── UI │ ├── DrawableTag.cs │ ├── Listing │ │ ├── DrawableListingEntry.cs │ │ └── ListingTab.cs │ ├── Menus │ │ └── LocalisableOsuMenuItem.cs │ ├── Overlay │ │ ├── CategorisedTabControlOverlayHeader.cs │ │ ├── RurusettoOverlay.cs │ │ ├── RurusettoOverlayBackground.cs │ │ ├── RurusettoOverlayHeader.cs │ │ └── RurusettoToolbarButton.cs │ ├── OverlayTab.cs │ ├── RequestFailedDrawable.cs │ ├── RulesetDownloadButton.cs │ ├── RulesetLogo.cs │ ├── RulesetManagementContextMenu.cs │ ├── RurusettoAddonConfigSubsection.cs │ ├── TogglableScrollContainer.cs │ ├── Users │ │ ├── DrawableRurusettoUser.cs │ │ ├── UserTab.cs │ │ └── VerifiedIcon.cs │ └── Wiki │ │ ├── HomeButton.cs │ │ ├── IssueButton.cs │ │ ├── MarkdownPage.cs │ │ ├── RecommendedBeatmapsPage.cs │ │ ├── WikiPage.cs │ │ ├── WikiSubpage.cs │ │ └── WikiTab.cs ├── osu.Game.Rulesets.RurusettoAddon.csproj └── osu.Game.Rulesets.RurusettoAddon.csproj.user └── overlayButton.png /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | .vscode/ 3 | bin/ 4 | obj/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Flutterish 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 | -------------------------------------------------------------------------------- /LocalizationGenerator/LocalisationGenerator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LocalizationGenerator/Program.cs: -------------------------------------------------------------------------------- 1 | using Humanizer; 2 | using ICSharpCode.Decompiler.Util; 3 | using Newtonsoft.Json; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | 10 | class Program { 11 | static void Main () { 12 | // TODO split this into several files (currently its 'Strings', but we might want 'Tags', 'Defaults' etc.) 13 | 14 | print( $"Loading {yellow( "'meta.jsonc'" )}..." ); 15 | var errors = new List(); 16 | 17 | Meta meta; 18 | try { 19 | meta = JsonConvert.DeserializeObject( File.ReadAllText( "./Source/meta.jsonc" ) ); 20 | print( $"Default locale: {meta.Default}" ); 21 | } 22 | catch ( Exception e ) { 23 | fail( $"Could not load {yellow( "'meta.jsonc'" )} - {e.Message}" ); 24 | return; 25 | } 26 | 27 | Dictionary localeFileNames = new(); 28 | Dictionary> localesWithKeys = new(); 29 | Dictionary> locales = new(); 30 | foreach ( var file in Directory.EnumerateFiles( "./Source/" ) ) { 31 | var filenameWithoutExtension = Path.GetFileNameWithoutExtension( file ); 32 | if ( filenameWithoutExtension == "meta" ) { 33 | continue; 34 | } 35 | 36 | var filename = Path.GetFileName( file ); 37 | print( "---------------" ); 38 | print( $"Loading {yellow( $"'{filename}'" )}..." ); 39 | Dictionary contents; 40 | try { 41 | contents = JsonConvert.DeserializeObject>( File.ReadAllText( file ) ); 42 | } 43 | catch ( Exception e ) { 44 | logError( $"Could not load {yellow( $"'{filename}'" )} - {e.Message}" ); 45 | continue; 46 | } 47 | 48 | if ( !contents.TryGetValue( "locale", out var locale ) ) { 49 | logError( $"'{filename}' does not contain a {yellow( "'locale'" )} key" ); 50 | continue; 51 | } 52 | contents.Remove( "locale" ); 53 | 54 | print( $"Locale: {locale}" ); 55 | if ( locale != filenameWithoutExtension ) { 56 | logError( $"File '{filename}' does not match its defined locale {yellow( $"'{locale}'" )}" ); 57 | } 58 | 59 | if ( locales.ContainsKey( locale ) ) { 60 | logError( $"File {yellow( $"'{filename}'" )} defines its locale as {yellow( $"'{locale}'" )}, but file {yellow( $"'{localeFileNames[locale]}'" )} already defined it" ); 61 | continue; 62 | } 63 | localeFileNames.Add( locale, filename ); 64 | locales.Add( locale, contents ); 65 | 66 | foreach ( var (key, value) in contents ) { 67 | if ( !localesWithKeys.TryGetValue( key, out var list ) ) { 68 | localesWithKeys.Add( key, list = new() ); 69 | } 70 | 71 | list.Add( locale ); 72 | } 73 | 74 | // TODO check argcount 75 | } 76 | 77 | print( "---------------" ); 78 | print( "Checking keys..." ); 79 | 80 | foreach ( var (key, localesWithKey) in localesWithKeys ) { 81 | if ( localesWithKey.Count != locales.Count ) { 82 | var defined = localesWithKey.Select( x => $"{localeFileNames[x]} ({x})" ); 83 | var undefined = locales.Keys.Except( localesWithKey ).Select( x => $"{localeFileNames[x]} ({x})" ); 84 | logError( $"The key {yellow( $"'{key}'" )} is defined in [{string.Join( ", ", defined )}] but not in [{string.Join( ", ", undefined )}]" ); 85 | } 86 | } 87 | 88 | if ( !locales.ContainsKey( meta.Default ) ) { 89 | fail( $"The default locale ({meta.Default}) was not found" ); 90 | return; 91 | } 92 | 93 | print( "---------------" ); 94 | 95 | string generateLocalisationClass () { 96 | var ns = $"osu.Game.Rulesets.RurusettoAddon.Localisation"; 97 | StringBuilder sb = new(); 98 | sb.AppendLine( $"using osu.Framework.Localisation;" ); 99 | sb.AppendLine(); 100 | sb.AppendLine( $"namespace {ns};" ); 101 | sb.AppendLine(); 102 | sb.AppendLine( $"public static class Strings {{" ); 103 | sb.AppendLine( $" private const string prefix = \"{ns}.Strings\";" ); 104 | foreach ( var (key, value) in locales[meta.Default] ) { 105 | sb.AppendLine( $" " ); 106 | if ( meta.Args.TryGetValue( key, out var args ) ) { 107 | sb.AppendLine( $" /// " ); 108 | sb.AppendLine( $" /// {value.Replace( "\n", "\n /// " )}" ); 109 | sb.AppendLine( $" /// " ); 110 | sb.AppendLine( $" public static LocalisableString {key.Replace( '-', '_' ).Pascalize()} ( {string.Join( ", ", args.Select( x => $"LocalisableString {x}" ) )} )" ); 111 | sb.AppendLine( $" => new TranslatableString( getKey( {JsonConvert.SerializeObject( key )} ), {JsonConvert.SerializeObject( value )}, {string.Join( ", ", args )} );" ); 112 | } 113 | else { 114 | sb.AppendLine( $" /// " ); 115 | sb.AppendLine( $" /// {value.Replace( "\n", "\n /// " )}" ); 116 | sb.AppendLine( $" /// " ); 117 | sb.AppendLine( $" public static LocalisableString {key.Replace( '-', '_' ).Pascalize()} => new TranslatableString( getKey( {JsonConvert.SerializeObject( key )} ), {JsonConvert.SerializeObject( value )} );" ); 118 | } 119 | } 120 | sb.AppendLine( $" " ); 121 | sb.AppendLine( $" private static string getKey ( string key ) => $\"{{prefix}}:{{key}}\";" ); 122 | sb.AppendLine( $"}}" ); 123 | 124 | return sb.ToString(); 125 | } 126 | 127 | var l12nDir = "."; 128 | 129 | while ( Path.GetFileName( Path.GetFullPath( l12nDir ) ) != "LocalizationGenerator" ) { 130 | l12nDir += "/.."; 131 | } 132 | 133 | l12nDir += "/../osu.Game.Rulesets.RurusettoAddon/Localisation/"; 134 | 135 | print( $"Generating files in {Path.GetFullPath( l12nDir )}\nContinue? [Y]/N" ); 136 | if ( Console.ReadLine() is "n" or "N" ) { 137 | fail( "Cancelled." ); 138 | return; 139 | } 140 | 141 | foreach ( var i in Directory.GetFiles( l12nDir ) ) { 142 | File.Delete( i ); 143 | } 144 | 145 | makeResxFile( $"Strings.resx", meta.Default ); 146 | foreach ( var (name, values) in locales ) { 147 | if ( name == meta.Default ) 148 | continue; 149 | 150 | makeResxFile( $"Strings.{name}.resx", name ); 151 | } 152 | 153 | void makeResxFile ( string filename, string locale ) { 154 | print( $"Generating {yellow( $"'{filename}'" )} ({locale})..." ); 155 | using var writer = new ResXResourceWriter( Path.Combine( l12nDir, filename ) ); 156 | 157 | foreach ( var (key, value) in locales[locale] ) { 158 | writer.AddResource( key, value ); 159 | } 160 | 161 | writer.Generate(); 162 | } 163 | 164 | File.WriteAllText( Path.Combine( l12nDir, "Strings.cs" ), generateLocalisationClass() ); 165 | 166 | logErrors(); 167 | print( "---------------" ); 168 | print( green( "[Done]" ) ); 169 | 170 | Console.ReadKey(); 171 | 172 | void logError ( string msg ) { 173 | print( red( $"[! Error]: {msg}" ) ); 174 | errors.Add( msg ); 175 | } 176 | 177 | void logErrors () { 178 | if ( errors.Any() ) { 179 | print( "---------------" ); 180 | foreach ( var i in errors ) { 181 | print( red( $"[! Error]: {i}" ) ); 182 | } 183 | } 184 | } 185 | 186 | void fail ( string msg ) { 187 | logErrors(); 188 | print( "---------------" ); 189 | print( red( $"[! Failure]: Could not finish - {msg}" ) ); 190 | } 191 | 192 | string red ( string msg ) { 193 | return $"{{}}{msg}{{}}"; 194 | } 195 | 196 | string green ( string msg ) { 197 | return $"{{}}{msg}{{}}"; 198 | } 199 | 200 | string yellow ( string msg ) { 201 | return $"{{}}{msg}{{}}"; 202 | } 203 | } 204 | 205 | static Stack textColors = new(); 206 | static void print ( string msg ) { 207 | string acc = ""; 208 | for ( int i = 0; i < msg.Length; i++ ) { 209 | if ( msg.Length - 2 > i && msg.AsSpan( i, 2 ).StartsWith( "{<" ) ) { 210 | Console.Write( acc ); 211 | acc = ""; 212 | i += 2; 213 | var command = msg.Substring( i, msg.IndexOf( ">}", i ) - i ); 214 | i += command.Length + 1; 215 | switch ( command ) { 216 | case "RED": 217 | textColors.Push( ConsoleColor.Red ); 218 | break; 219 | case "GREEN": 220 | textColors.Push( ConsoleColor.Green ); 221 | break; 222 | case "YELLOW": 223 | textColors.Push( ConsoleColor.DarkYellow ); 224 | break; 225 | case "POP": 226 | textColors.Pop(); 227 | break; 228 | } 229 | Console.ForegroundColor = textColors.Any() ? textColors.Peek() : ConsoleColor.White; 230 | } 231 | else { 232 | acc += msg[i]; 233 | } 234 | } 235 | Console.WriteLine( acc ); 236 | } 237 | } 238 | 239 | class Meta { 240 | [JsonProperty( "default" )] 241 | public string Default; 242 | [JsonProperty( "comments" )] 243 | public Dictionary Comments; 244 | [JsonProperty( "args" )] 245 | public Dictionary Args; 246 | } -------------------------------------------------------------------------------- /LocalizationGenerator/Source/en.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "locale": "en", 3 | 4 | "local-ruleset-description": "Local ruleset, not listed on the wiki.", 5 | // {0} = link to the wiki page 6 | "page-load-failed": "Failed to fetch the ruleset wiki page. Sorry!\nYou can still try visiting it at [rurusetto]({0}).", 7 | "listing-tab": "listing", 8 | "users-tab": "users", 9 | "collections-tab": "collections", 10 | "rurusetto-description": "browse and manage rulesets", 11 | "user-unknown": "Unknown", 12 | "tag-archived": "ARCHIVED", 13 | "tag-archived-tooltip": "This ruleset is no longer maintained", 14 | "tag-local": "UNLISTED", 15 | "tag-local-tooltip": "This ruleset is installed locally, but is not listed on the wiki", 16 | "tag-hardcoded": "HARD CODED", 17 | "tag-hardcoded-tooltip": "This ruleset is hard coded into the game and cannot be modified", 18 | "tag-failed-import": "FAILED IMPORT", 19 | "tag-failed-import-tooltip": "This ruleset is downloaded, but failed to import", 20 | "tag-borked": "BORKED", 21 | "tag-borked-tooltip": "This ruleset does not work", 22 | "tag-playable": "PLAYABLE", 23 | "tag-playable-tooltip": "This ruleset works", 24 | "tag-prerelease": "PRE-RELEASE", 25 | "tag-prerelease-tooltip": "The current version is a pre-release", 26 | "home-page": "Home Page", 27 | "report-issue": "Report Issue", 28 | "download-checking": "Checking...", 29 | "unavailable-online": "Unavailable Online", 30 | "installed-unavailable-online": "Installed, not available online", 31 | "download": "Download", 32 | "redownload": "Re-download", 33 | "downloading": "Downloading...", 34 | "update": "Update", 35 | "remove": "Remove", 36 | "cancel-download": "Cancel Download", 37 | "cancel-remove": "Cancel Removal", 38 | "cancel-update": "Cancel Update", 39 | "refresh": "Refresh", 40 | "to-be-removed": "Will be removed on restart!", 41 | "to-be-installed": "Will be installed on restart!", 42 | "to-be-updated": "Will be updated on restart!", 43 | "installed": "Installed", 44 | "outdated": "Outdated", 45 | "creator-verified": "Verified Ruleset Creator", 46 | // {0} = error code 47 | "load-error": "Could not load rurusetto-addon: Please report this to the rurusetto-addon repository NOT the osu!lazer repository: Code {0}", 48 | "main-page": "Main", 49 | "changelog-page": "Changelog", 50 | "recommended-beatmaps-page": "Recommended Beatmaps", 51 | "unknown-version": "Unknown Version", 52 | "settings-header": "Rurusetto Addon", 53 | "settings-api-address": "API Address", 54 | "untitled-ruleset": "Untitled Ruleset", 55 | "error-header": "Oh no!", 56 | "error-footer": "Please make sure you have an internet connection and the API address in settings is correct", 57 | "retry": "Retry", 58 | "listing-fetch-error": "Could not retrieve the ruleset listing", 59 | "page-fetch-error": "Could not load the page", 60 | "subpages-fetch-error": "Could not retrieve subpages", 61 | "error-message-generic": "Something went wrong, but I don't know what!", 62 | "notification-work-incomplete": "It seems rurusetto addon couldn't finish some work. Please make sure all your changes were applied correctly" 63 | } -------------------------------------------------------------------------------- /LocalizationGenerator/Source/es.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "locale": "es", 3 | 4 | "local-ruleset-description": "Ruleset local, no listado en la wiki.", 5 | // {0} = link to the wiki page 6 | "page-load-failed": "No se pudo obtener la página de la wiki del ruleset. ¡Lo siento!\nTodavía puedes intentar visitarla en [rurusetto]({0}).", 7 | "listing-tab": "listado", 8 | "users-tab": "usuarios", 9 | "collections-tab": "colecciones", 10 | "rurusetto-description": "explorar y administrar rulesets", 11 | "user-unknown": "Desconocido", 12 | "tag-archived": "ARCHIVADO", 13 | "tag-archived-tooltip": "Este ruleset ya no se mantiene", 14 | "tag-local": "NO LISTADO", 15 | "tag-local-tooltip": "Este ruleset está instalado localmente, pero no está listado en la wiki", 16 | "tag-hardcoded": "CODIFICADO", 17 | "tag-hardcoded-tooltip": "Este ruleset está codificado en el juego y no se puede modificar.", 18 | "tag-failed-import": "IMPORTACIÓN FALLIDA", 19 | "tag-failed-import-tooltip": "Este ruleset se descargó, pero no se pudo importar", 20 | "tag-borked": "BORRADO", 21 | "tag-borked-tooltip": "Este ruleset no funciona.", 22 | "tag-playable": "JUGABLE", 23 | "tag-playable-tooltip": "Este ruleset funciona", 24 | "tag-prerelease": "PRE-LANZAMIENTO", 25 | "tag-prerelease-tooltip": "La versión actual es un pre-lanzamiento.", 26 | "home-page": "Página de inicio", 27 | "report-issue": "Reportar problema", 28 | "download-checking": "Comprobando...", 29 | "unavailable-online": "No disponible en línea", 30 | "installed-unavailable-online": "Instalado, no disponible en línea", 31 | "download": "Descargar", 32 | "redownload": "Re-descargar", 33 | "downloading": "Descargando...", 34 | "update": "Actualizar", 35 | "remove": "Eliminar", 36 | "cancel-download": "Cancelar descarga", 37 | "cancel-remove": "Cancelar eliminación", 38 | "cancel-update": "Cancelar actualización", 39 | "refresh": "Actualizar", 40 | "to-be-removed": "Se eliminará al reiniciar!", 41 | "to-be-installed": "Se instalará al reiniciar!", 42 | "to-be-updated": "Se actualizará al restart!", 43 | "installed": "Instalado", 44 | "outdated": "Desactualizado", 45 | "creator-verified": "Creador de rulesets verificado", 46 | // {0} = error code 47 | "load-error": "No se pudo cargar rurusetto-addon: Informe esto al repositorio de rurusetto-addon NO al repositorio de osu!lazer: Código {0}", 48 | "main-page": "Principal", 49 | "changelog-page": "Registro de cambios", 50 | "recommended-beatmaps-page": "Beatmaps recomendados", 51 | "unknown-version": "Versión desconocida", 52 | "settings-header": "Rurusetto Addon", 53 | "settings-api-address": "Dirección de la API", 54 | "untitled-ruleset": "Ruleset sin título", 55 | "error-header": "Oh no!", 56 | "error-footer": "Asegúrese de tener una conexión a Internet y que la dirección de la API en la configuración sea correcta.", 57 | "retry": "Reintentar", 58 | "listing-fetch-error": "No se pudo recuperar el listado de rulesets", 59 | "page-fetch-error": "No se pudo cargar la página", 60 | "subpages-fetch-error": "No se pudieron recuperar las subpáginas", 61 | "error-message-generic": "Algo salió mal, pero no sé qué!", 62 | "notification-work-incomplete": "Parece que rurusetto addon no pudo terminar un trabajo. Por favor, asegúrese de que todos sus cambios se aplicaron correctamente" 63 | } 64 | -------------------------------------------------------------------------------- /LocalizationGenerator/Source/meta.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "default": "en", 3 | "args": { 4 | "page-load-failed": [ "wikiLink" ], 5 | "load-error": [ "errorCode" ] 6 | } 7 | } -------------------------------------------------------------------------------- /LocalizationGenerator/Source/pl.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "locale": "pl", 3 | 4 | "local-ruleset-description": "Lokalny tryb gry, nie wymieniony na wiki.", 5 | // {0} = link to the wiki page 6 | "page-load-failed": "Nie udało się pobrać strony o tym trybie gry.\nJeśli chcesz, ciągle możesz spróbować na oficjalnej stronie [rurusetto]({0}).", 7 | "listing-tab": "katalog", 8 | "rurusetto-description": "przeglądaj i zarządzaj trybami gry", 9 | "user-unknown": "Nieznany Użytkownik", 10 | "tag-archived": "ARCHIWUM", 11 | "tag-archived-tooltip": "Ten tryb gry nie jest już utrzymywany", 12 | "tag-local": "LOKALNY", 13 | "tag-local-tooltip": "Ten tryb gry jest zainstalowany lokalnie, ale nie ma go na wiki", 14 | "tag-hardcoded": "WBUDOWANY", 15 | "tag-hardcoded-tooltip": "Ten tryb gry jest wbudowany w grę i nie da się go modyfikować", 16 | "tag-failed-import": "BŁĄD IMPORTU", 17 | "tag-failed-import-tooltip": "Ten tryb gry jest pobrany, ale nie udało się go zaimportować", 18 | "tag-borked": "ZDZBANIONY", 19 | "tag-borked-tooltip": "Ten tryb gry nie działa", 20 | "tag-playable": "GRYWALNY", 21 | "tag-playable-tooltip": "Ten tryb gry działa", 22 | "tag-prerelease": "PRE-RELEASE", 23 | "tag-prerelease-tooltip": "Obecna wersja tego trybu gry jest niedokończona", 24 | "home-page": "Strona domowa", 25 | "report-issue": "Zgłoś problem", 26 | "download-checking": "Sprawdzanie...", 27 | "unavailable-online": "Niedostępny Online", 28 | "installed-unavailable-online": "Zainstalowany, ale niedostępny online", 29 | "download": "Pobierz", 30 | "redownload": "Pobierz ponownie", 31 | "downloading": "Pobieranie...", 32 | "update": "Uaktualnij", 33 | "remove": "Usuń", 34 | "cancel-download": "Anuluj Pobieranie", 35 | "cancel-remove": "Anuluj Usuwanie", 36 | "cancel-update": "Anujuj Aktualizacje", 37 | "refresh": "Odśwież", 38 | "to-be-removed": "Zostanie usunięty przy następnym uruchomieniu!", 39 | "to-be-installed": "Zostanie zainstalowany przy następnym uruchomieniu!", 40 | "to-be-updated": "Zostanie uaktualniony przy następnym uruchomieniu!", 41 | "installed": "Zainstalowany", 42 | "outdated": "Przestarzały", 43 | "creator-verified": "Zweryfikowani Twórcy Trybu", 44 | // {0} = error code 45 | "load-error": "Nie udało się uruchomić rurusetto-addon: Proszę, zgłoś to do repozytorium rurusetto-addon i NIE do repozytorium osu!lazer: Kod {0}", 46 | "main-page": "Główna", 47 | "changelog-page": "Zmiany", 48 | "recommended-beatmaps-page": "Polecane Mapy", 49 | "unknown-version": "Nieznana Wersja", 50 | "settings-header": "Zintegrowane Rurusetto", 51 | "settings-api-address": "Adres API", 52 | "untitled-ruleset": "Nienazwany Tryb Gry", 53 | "error-header": "Ups!", 54 | "error-footer": "Upewnij się, że masz połączenie z internetem, a adres API w ustawieniach jest poprawny", 55 | "retry": "Odśwież", 56 | "listing-fetch-error": "Nie udało się pobrać katalogu", 57 | "page-fetch-error": "Nie udało się załadować strony", 58 | "subpages-fetch-error": "Nie udało się pobrać listy podstron", 59 | "error-message-generic": "Coś poszło nie tak, ale nie wiem co!", 60 | "notification-work-incomplete": "Wygląda na to, że wtyczka rurusetto nie dokończyła pracy. Sprawdź, czy wszystkie twoje zmiany zostały pomyślnie zaaplikowane" 61 | } -------------------------------------------------------------------------------- /LocalizationGenerator/Source/ru.jsonc: -------------------------------------------------------------------------------- 1 | /// Author: https://github.com/Loreos7 2 | 3 | { 4 | "locale": "ru", 5 | 6 | "local-ruleset-description": "Локальный режим, которого нет на вики", 7 | // {0} = link to the wiki page 8 | "page-load-failed": "Не удалось загрузить страницу режима на вики. Извините!\nПопробуйте открыть ее на сайте [rurusetto]({1}).", 9 | "listing-tab": "список", 10 | "rurusetto-description": "открыть список доступных режимов", 11 | "user-unknown": "Аноним", 12 | "tag-archived": "В АРХИВЕ", 13 | "tag-archived-tooltip": "Этот режим больше не поддерживается", 14 | "tag-local": "ЛОКАЛЬНЫЙ", 15 | "tag-local-tooltip": "Этот режим установлен только у вас, его нет на вики", 16 | "tag-hardcoded": "ВСТРОЕННЫЙ", 17 | "tag-hardcoded-tooltip": "Этот режим встроен в игру и не может быть изменён", 18 | "tag-failed-import": "ОШИБКА ИМПОРТА", 19 | "tag-failed-import-tooltip": "Режим скачан, но его не удалось импортировать", 20 | "tag-borked": "НЕРАБОЧИЙ", 21 | "tag-borked-tooltip": "Этот режим не работает", 22 | "tag-playable": "РАБОЧИЙ", 23 | "tag-playable-tooltip": "Этот режим работает", 24 | "tag-prerelease": "БЕТА", 25 | "tag-prerelease-tooltip": "Текущая версия находится в стадии активной разработки", 26 | "home-page": "Домашняя страница", 27 | "report-issue": "Сообщить о проблеме", 28 | "download-checking": "Проверка...", 29 | "unavailable-online": "Недоступно для прямого скачивания", 30 | "installed-unavailable-online": "Установлен, не доступен онлайн", 31 | "download": "Скачать", 32 | "redownload": "Скачать заново", 33 | "downloading": "Загрузка...", 34 | "update": "Скачать обновление", 35 | "remove": "Удалить", 36 | "cancel-download": "Отменить загрузку", 37 | "cancel-remove": "Отменить удаление", 38 | "cancel-update": "Отменить обновление", 39 | "refresh": "Обновить статус", 40 | "to-be-removed": "Будет удалён при следующем входе в игру!", 41 | "to-be-installed": "Будет установлен при следующем входе в игру!", 42 | "to-be-updated": "Будет обновлён при следующем входе в игру!", 43 | "installed": "Установлен", 44 | "outdated": "Устарел", 45 | "creator-verified": "Подтверждённый создатель режима", 46 | // {0} = error code 47 | "load-error": "Не удалось загрузить rurusetto-addon: Пожалуйста, сообщите об этом в репозиторий rurusetto-addon, НЕ в репозиторий osu!lazer: Code {1}", 48 | "main-page": "Главная", 49 | "changelog-page": "Список изменений", 50 | "unknown-version": "Версия неизвестна", 51 | "settings-header": "Rurusetto Addon", 52 | "settings-api-address": "Адрес API", 53 | "untitled-ruleset": "Безымянный режим", 54 | "error-header": "О нет!", 55 | "error-footer": "Пожалуйста, убедитесь, что у вас есть подключение к интернету и адрес API в настройках не содержит ошибок", 56 | "retry": "Обновить", 57 | "listing-fetch-error": "Не удалось загрузить список режимов", 58 | "page-fetch-error": "Не удалось загрузить страницу", 59 | "subpages-fetch-error": "Не удалось загрузить подстраницы", 60 | "error-message-generic": "Что-то пошло не так, и я не знаю, что именно!", 61 | "notification-work-incomplete": "Кажется, rurusetto addon не смог завершить предыдущую рабочую сессию. Пожалуйста, убедитесь, что ваши действия не могли вызвать ошибок", 62 | "recommended-beatmaps-page": "Рекомендуемые карты" 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rūruestto Addon 2 | An addon for osu!lazer which allows you to browse the [rūrusetto](https://rulesets.info) wiki in-game and manage your rulesets. 3 | 4 | ## Special thanks 5 | To [Nao](https://github.com/naoei) for graphical design for the addon and [Yulianna](https://github.com/HelloYeew) for making Rurūsetto. 6 | To [Loreos](https://github.com/Loreos7) for the russian locale. 7 | 8 | ## Installing 9 | * Open osu!lazer 10 | * Go to settings 11 | * Click `Open osu! foler` at the bottom of `General` settings 12 | * Open the `rulesets` folder 13 | * Copy the path 14 | * Go to [Releases](/releases) of this repository 15 | * Click the topmost one 16 | * Download the `osu.Game.Rulesets.RurusettoAddon.dll` file. Save to the copied path. 17 | * Restart osu! 18 | * Done! Click the ![overlay button](./overlayButton.png) button to browse the wiki 19 | 20 | You can also install and update locale (translation) files by following the same steps for the `.zip` files included with each release. 21 | 22 | https://user-images.githubusercontent.com/40297338/149041138-003f4a3e-3144-4139-8558-34d13da8d40f.mp4 23 | 24 | ## Contributing 25 | If you can code, you can contribute the standard [github way](https://github.com/firstcontributions/first-contributions): 26 | * Fork this repository 27 | * Apply code changes 28 | * Open a PR (Pull Request) to submit your changes for review 29 | 30 | If you can't code, you can still contribute localisation (translation files). You still need to follow the steps outlined in [here](https://github.com/firstcontributions/first-contributions): 31 | * Fork this repository 32 | * Open the [Localisation Generator Source folder](./LocalizationGenerator/Source) 33 | * The files in this folder are named after the locale it uses. For example english uses `en.jsonc`. For a list of ISO language codes, see [this website](http://www.lingoes.net/en/translator/langcode.htm) or if you can't find your locale there, wikipedia has a list (although it it a bit harder to read than this site) 34 | * If your locale is not there, create a file following the naming convention outlined above. Initialize the file with `{ "locale": "ISO language code" }` 35 | * Open the file. It can be opened in a regular text editor, although we recommend [VS Code](https://code.visualstudio.com) 36 | * You probably also want to open `en.jsonc` or any other translation file as a reference 37 | * If you can build, you can run `LocalisationGenerator.csproj` 38 | * The program will perform some checks to see what resources are missing across all localisation files 39 | * You can exit the program. You don't need to confirm to generate files just yet 40 | * Add translations or typecheck your file 41 | * Since the format is `.jsonc` and not `.json`, you can add comments to the file if you want by typing `// this is my comment!` anywhere. Even though github credits commit authors, feel free to credit yourself at the top of the file with a triple slash comment 42 | * Some strings contain a `{0}` or `{1}` etc. These are going to be replaced by something such as a number, a link or an error code. All entries which contain them must be commented by explaining what they are 43 | * If you can build, run `LocalisationGenerator.csproj`. This time confirm and generate resource files. If you do not know how to build, a collaborator will do this step for you when you submit your PR 44 | * Open a PR (Pull Request) to submit your changes for review 45 | 46 | You can also contribute to the [rūrusetto wiki](https://rulesets.info) or any of the rulesets you discover! Make sure to show some ❤️ to the awesome people who develop and maintain them. 47 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon.Tests/TestSceneOsuGame.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Shapes; 3 | using osu.Game; 4 | using osu.Game.Tests.Visual; 5 | using osuTK.Graphics; 6 | 7 | public class TestSceneOsuGame : OsuTestScene { 8 | protected override void LoadComplete () { 9 | base.LoadComplete(); 10 | 11 | Children = new Drawable[] { 12 | new Box { 13 | RelativeSizeAxes = Axes.Both, 14 | Colour = Color4.Black, 15 | } 16 | }; 17 | AddGame( new OsuGame() ); 18 | } 19 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon.Tests/TestSceneOverlay.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Rulesets.RurusettoAddon; 2 | using osu.Game.Rulesets.RurusettoAddon.UI.Overlay; 3 | using osu.Game.Tests.Visual; 4 | 5 | public class TestSceneOverlay : OsuTestScene { 6 | RurusettoOverlay overlay; 7 | public TestSceneOverlay () { 8 | Add( overlay = new RurusettoOverlay( new RurusettoAddonRuleset() ) ); 9 | overlay.Show(); 10 | } 11 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon.Tests/VisualTestRunner.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework; 2 | using osu.Framework.Platform; 3 | using osu.Game.Tests; 4 | 5 | using DesktopGameHost host = Host.GetSuitableDesktopHost( @"osu", new() { BindIPC = true } ); 6 | host.Run( new OsuTestBrowser() ); -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon.Tests/osu.Game.Rulesets.RurusettoAddon.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | false 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | WinExe 21 | net6.0 22 | osu.Game.Rulesets.RurusettoAddon.Tests 23 | 24 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29123.88 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.RurusettoAddon", "osu.Game.Rulesets.RurusettoAddon\osu.Game.Rulesets.RurusettoAddon.csproj", "{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.RurusettoAddon.Tests", "osu.Game.Rulesets.RurusettoAddon.Tests\osu.Game.Rulesets.RurusettoAddon.Tests.csproj", "{B4577C85-CB83-462A-BCE3-22FFEB16311D}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LocalisationGenerator", "LocalizationGenerator\LocalisationGenerator.csproj", "{291BB741-D6F4-4BA0-91F2-D598B3777284}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | VisualTests|Any CPU = VisualTests|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.ActiveCfg = Release|Any CPU 24 | {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.Build.0 = Release|Any CPU 25 | {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.ActiveCfg = Debug|Any CPU 30 | {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.Build.0 = Debug|Any CPU 31 | {291BB741-D6F4-4BA0-91F2-D598B3777284}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {291BB741-D6F4-4BA0-91F2-D598B3777284}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {291BB741-D6F4-4BA0-91F2-D598B3777284}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {291BB741-D6F4-4BA0-91F2-D598B3777284}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {291BB741-D6F4-4BA0-91F2-D598B3777284}.VisualTests|Any CPU.ActiveCfg = Debug|Any CPU 36 | {291BB741-D6F4-4BA0-91F2-D598B3777284}.VisualTests|Any CPU.Build.0 = Debug|Any CPU 37 | EndGlobalSection 38 | GlobalSection(SolutionProperties) = preSolution 39 | HideSolutionNode = FALSE 40 | EndGlobalSection 41 | GlobalSection(ExtensibilityGlobals) = postSolution 42 | SolutionGuid = {671B0BEC-2403-45B0-9357-2C97CC517668} 43 | EndGlobalSection 44 | GlobalSection(MonoDevelopProperties) = preSolution 45 | Policies = $0 46 | $0.TextStylePolicy = $1 47 | $1.EolMarker = Windows 48 | $1.inheritsSet = VisualStudio 49 | $1.inheritsScope = text/plain 50 | $1.scope = text/x-csharp 51 | $0.CSharpFormattingPolicy = $2 52 | $2.IndentSwitchSection = True 53 | $2.NewLinesForBracesInProperties = True 54 | $2.NewLinesForBracesInAccessors = True 55 | $2.NewLinesForBracesInAnonymousMethods = True 56 | $2.NewLinesForBracesInControlBlocks = True 57 | $2.NewLinesForBracesInAnonymousTypes = True 58 | $2.NewLinesForBracesInObjectCollectionArrayInitializers = True 59 | $2.NewLinesForBracesInLambdaExpressionBody = True 60 | $2.NewLineForElse = True 61 | $2.NewLineForCatch = True 62 | $2.NewLineForFinally = True 63 | $2.NewLineForMembersInObjectInit = True 64 | $2.NewLineForMembersInAnonymousTypes = True 65 | $2.NewLineForClausesInQuery = True 66 | $2.SpacingAfterMethodDeclarationName = False 67 | $2.SpaceAfterMethodCallName = False 68 | $2.SpaceBeforeOpenSquareBracket = False 69 | $2.inheritsSet = Mono 70 | $2.inheritsScope = text/x-csharp 71 | $2.scope = text/x-csharp 72 | EndGlobalSection 73 | GlobalSection(MonoDevelopProperties) = preSolution 74 | Policies = $0 75 | $0.TextStylePolicy = $1 76 | $1.EolMarker = Windows 77 | $1.inheritsSet = VisualStudio 78 | $1.inheritsScope = text/plain 79 | $1.scope = text/x-csharp 80 | $0.CSharpFormattingPolicy = $2 81 | $2.IndentSwitchSection = True 82 | $2.NewLinesForBracesInProperties = True 83 | $2.NewLinesForBracesInAccessors = True 84 | $2.NewLinesForBracesInAnonymousMethods = True 85 | $2.NewLinesForBracesInControlBlocks = True 86 | $2.NewLinesForBracesInAnonymousTypes = True 87 | $2.NewLinesForBracesInObjectCollectionArrayInitializers = True 88 | $2.NewLinesForBracesInLambdaExpressionBody = True 89 | $2.NewLineForElse = True 90 | $2.NewLineForCatch = True 91 | $2.NewLineForFinally = True 92 | $2.NewLineForMembersInObjectInit = True 93 | $2.NewLineForMembersInAnonymousTypes = True 94 | $2.NewLineForClausesInQuery = True 95 | $2.SpacingAfterMethodDeclarationName = False 96 | $2.SpaceAfterMethodCallName = False 97 | $2.SpaceBeforeOpenSquareBracket = False 98 | $2.inheritsSet = Mono 99 | $2.inheritsScope = text/x-csharp 100 | $2.scope = text/x-csharp 101 | EndGlobalSection 102 | EndGlobal 103 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/API/APIRuleset.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Textures; 2 | using osu.Framework.Localisation; 3 | using osu.Game.Rulesets.RurusettoAddon.UI; 4 | using static osu.Game.Rulesets.RurusettoAddon.API.RurusettoAPI; 5 | 6 | namespace osu.Game.Rulesets.RurusettoAddon.API; 7 | 8 | public class APIRuleset { 9 | public Source Source; 10 | 11 | public RurusettoAPI? API; 12 | public string? Slug; 13 | 14 | public LocalisableString Name = Localisation.Strings.UntitledRuleset; 15 | public string? LocalPath; 16 | public string? ShortName; 17 | public bool IsModifiable; 18 | public bool IsPresentLocally; 19 | public bool HasImportFailed; 20 | 21 | public IRulesetInfo? LocalRulesetInfo; 22 | public ListingEntry? ListingEntry; 23 | 24 | // ListingEntry properties 25 | 26 | /// 27 | public UserDetail? Owner => ListingEntry?.Owner; 28 | /// 29 | public bool IsVerified => ListingEntry?.IsVerified ?? false; 30 | /// 31 | public LocalisableString Description => string.IsNullOrWhiteSpace( ListingEntry?.Description ) ? Localisation.Strings.LocalRulesetDescription : ListingEntry.Description; 32 | /// 33 | public bool CanDownload => ListingEntry?.CanDownload ?? false; 34 | /// 35 | public string? Download => ListingEntry?.Download; 36 | 37 | /// 38 | /// Merges info from into itself 39 | /// 40 | public void Merge ( APIRuleset other ) { 41 | Source = other.Source; 42 | Slug = other.Slug; 43 | Name = other.Name; 44 | LocalPath = other.LocalPath; 45 | ShortName = other.ShortName; 46 | IsModifiable = other.IsModifiable; 47 | IsPresentLocally = other.IsPresentLocally; 48 | HasImportFailed = other.HasImportFailed; 49 | LocalRulesetInfo = other.LocalRulesetInfo; 50 | ListingEntry = other.ListingEntry; 51 | } 52 | 53 | /// 54 | /// Creates the dark mode variant of the ruleset logo as a drawable with relative size axes 55 | /// 56 | public void RequestDarkLogo ( Action success, Action? failure = null, bool useLocalIcon = true ) { 57 | static Drawable createDefault () { 58 | return new SpriteIcon { 59 | RelativeSizeAxes = Axes.Both, 60 | FillMode = FillMode.Fit, 61 | Icon = FontAwesome.Solid.QuestionCircle 62 | }; 63 | } 64 | 65 | if ( LocalRulesetInfo != null && useLocalIcon ) { 66 | var icon = LocalRulesetInfo.CreateInstance()?.CreateIcon(); 67 | 68 | if ( icon is CompositeDrawable cd && cd.AutoSizeAxes != Axes.None ) { 69 | var container = new Container { 70 | RelativeSizeAxes = Axes.Both, 71 | Child = icon 72 | }; 73 | 74 | container.OnUpdate += c => { 75 | c.Scale = Vector2.Divide( c.DrawSize, icon.DrawSize ); 76 | }; 77 | 78 | success( container ); 79 | } 80 | else if ( icon != null ) { 81 | icon.RelativeSizeAxes = Axes.Both; 82 | icon.Size = new Vector2( 1 ); 83 | 84 | success( icon ); 85 | } 86 | else { 87 | failure?.Invoke( createDefault() ); 88 | } 89 | } 90 | else if ( !string.IsNullOrWhiteSpace( ListingEntry?.DarkIcon ) && API != null ) { 91 | API.RequestImage( ListingEntry.DarkIcon, logo => { 92 | success( new Sprite { 93 | RelativeSizeAxes = Axes.Both, 94 | FillMode = FillMode.Fit, 95 | Texture = logo 96 | } ); 97 | }, e => { 98 | failure?.Invoke( createDefault() ); 99 | } ); 100 | } 101 | else { 102 | failure?.Invoke( createDefault() ); 103 | } 104 | } 105 | 106 | public void RequestDetail ( Action success, Action? failure = null ) { 107 | if ( Source == Source.Web && !string.IsNullOrWhiteSpace( Slug ) && API != null ) { 108 | API.RequestRulesetDetail( Slug, success, failure ); 109 | // TODO this on failures 110 | //API.FlushRulesetDetailCache( Slug ); 111 | //new RulesetDetail { 112 | // CanDownload = CanDownload, 113 | // Download = Download, 114 | // Content = $"Failed to fetch the ruleset wiki page. Sorry!\nYou can still try visiting it at [rurusetto]({API.Address.Value.TrimEnd('/')}/rulesets/{Slug}).", 115 | // CreatedAt = DateTime.Now, 116 | // Description = Description, 117 | // LastEditedAt = DateTime.Now, 118 | // Name = Name, 119 | // Slug = Slug 120 | //}; 121 | } 122 | else { 123 | success( new RulesetDetail { 124 | CanDownload = CanDownload, 125 | Download = Download, 126 | Content = Localisation.Strings.LocalRulesetDescription, 127 | CreatedAt = DateTime.Now, 128 | Description = Description, 129 | LastEditedAt = DateTime.Now, 130 | Name = Name, 131 | Slug = Slug 132 | } ); 133 | } 134 | } 135 | 136 | public void RequestSubpages ( Action> success, Action? failure = null ) { 137 | if ( Source == Source.Web && API != null && !string.IsNullOrWhiteSpace( Slug ) ) { 138 | API.RequestSubpageListing( Slug, success, failure ); 139 | } 140 | else { 141 | success( Array.Empty() ); 142 | } 143 | } 144 | public void FlushSubpageListing () { 145 | if ( Source == Source.Web && API != null && !string.IsNullOrWhiteSpace( Slug ) ) { 146 | API.FlushSubpageListingCache( Slug ); 147 | } 148 | } 149 | public void FlushSubpage ( string slug ) { 150 | if ( Source == Source.Web && API != null && !string.IsNullOrWhiteSpace( Slug ) ) { 151 | API.FlushSubpageCache( Slug, slug ); 152 | } 153 | } 154 | public void FlushSubpages () { // NOTE this doesnt refresh changlog and main 155 | if ( Source == Source.Web && API != null && !string.IsNullOrWhiteSpace( Slug ) ) { 156 | API.FlushSubpageCache( Slug ); 157 | } 158 | } 159 | 160 | public void RequestSubpage ( string subpageSlug, Action success, Action? failure = null ) { 161 | if ( Source == Source.Web && API != null && !string.IsNullOrWhiteSpace( Slug ) && !string.IsNullOrWhiteSpace( subpageSlug ) ) { 162 | API.RequestSubpage( Slug, subpageSlug, success, failure ); 163 | } 164 | else { 165 | failure?.Invoke( null ); 166 | } 167 | } 168 | 169 | public void RequestRecommendations ( RecommendationSource source, Action> success, Action? failure = null ) { 170 | if ( Source == Source.Web && API != null && !string.IsNullOrWhiteSpace( Slug ) ) { 171 | API.RequestBeatmapRecommendations( Slug, source, success, failure ); 172 | } 173 | else { 174 | failure?.Invoke( null ); 175 | } 176 | } 177 | public void FlushRecommendations () { 178 | if ( Source == Source.Web && API != null && !string.IsNullOrWhiteSpace( Slug ) ) { 179 | API.FlushBeatmapRecommendationsCache( Slug ); 180 | } 181 | } 182 | 183 | public void RequestDarkCover ( Action success, Action? failure = null ) { 184 | if ( API is null ) { 185 | failure?.Invoke( null ); 186 | } 187 | else { 188 | RequestDetail( detail => { 189 | if ( string.IsNullOrWhiteSpace( detail.CoverDark ) ) { 190 | API.RequestImage( StaticAPIResource.DefaultCover, success, failure ); 191 | } 192 | else { 193 | API.RequestImage( detail.CoverDark, success, failure ); 194 | } 195 | }, failure ); 196 | } 197 | } 198 | 199 | public IEnumerable GenerateTags ( RulesetDetail detail, bool large = false, bool includePlayability = true ) { 200 | if ( Source == Source.Local ) { 201 | yield return DrawableTag.CreateLocal( large ); 202 | } 203 | if ( LocalRulesetInfo != null && !IsModifiable ) { 204 | yield return DrawableTag.CreateHardCoded( large ); 205 | } 206 | if ( HasImportFailed ) { 207 | yield return DrawableTag.CreateFailledImport( large ); 208 | } 209 | 210 | if ( detail.IsArchived ) { 211 | yield return DrawableTag.CreateArchived( large ); 212 | } 213 | if ( ListingEntry != null ) { 214 | if ( includePlayability && ListingEntry.Status.IsBorked ) { 215 | yield return DrawableTag.CreateBorked( large ); 216 | } 217 | if ( ListingEntry.Status.IsPrerelease ) { 218 | yield return DrawableTag.CreatePrerelease( large ); 219 | } 220 | } 221 | } 222 | 223 | public override string ToString () 224 | => $"{Name} [Source {Source}] [{( Source is Source.Web ? $"Slug {Slug}" : $"Path {LocalPath}" )}]"; 225 | } 226 | 227 | public enum Source { 228 | Web, 229 | Local 230 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/API/APIUser.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Textures; 2 | 3 | namespace osu.Game.Rulesets.RurusettoAddon.API; 4 | 5 | public class APIUser { 6 | private APIUser () { } 7 | public static APIUser FromID ( RurusettoAPI API, int ID ) { 8 | return new() { 9 | Source = Source.Web, 10 | API = API, 11 | ID = ID, 12 | HasProfile = true 13 | }; 14 | } 15 | public static APIUser Local ( RurusettoAPI API ) { 16 | return new() { 17 | Source = Source.Local, 18 | API = API 19 | }; 20 | } 21 | 22 | public Source Source { get; private set; } 23 | private int ID; 24 | private RurusettoAPI? API; 25 | 26 | public bool HasProfile { get; private set; } 27 | 28 | public void RequestDetail ( Action success, Action? failure = null ) { 29 | if ( Source == Source.Web && API != null ) { 30 | API.RequestUserProfile( ID, success, failure ); 31 | } 32 | else { 33 | success( new() ); 34 | } 35 | } 36 | 37 | public void RequestProfilePicture ( Action success, Action? failure = null ) { 38 | void requestDefault ( Exception? e = null ) { 39 | if ( API != null ) { 40 | API.RequestImage( StaticAPIResource.DefaultProfileImage, success, failure: e => { 41 | failure?.Invoke( e ); 42 | } ); 43 | } 44 | } 45 | 46 | if ( Source == Source.Web && API != null ) { 47 | RequestDetail( detail => { 48 | if ( !string.IsNullOrWhiteSpace( detail.ProfilePicture ) ) { 49 | API.RequestImage( detail.ProfilePicture, success, requestDefault ); 50 | } 51 | else { 52 | requestDefault(); 53 | } 54 | }, failure: requestDefault ); 55 | } 56 | else { 57 | requestDefault(); 58 | } 59 | } 60 | 61 | public void RequestDarkCover ( Action success, Action? failure = null ) { 62 | void requestDefault ( Exception? e = null ) { 63 | if ( API != null ) { 64 | API.RequestImage( StaticAPIResource.DefaultCover, success, failure: e => { 65 | failure?.Invoke( e ); 66 | } ); 67 | } 68 | } 69 | 70 | if ( Source == Source.Web && API != null ) { 71 | RequestDetail( detail => { 72 | if ( !string.IsNullOrWhiteSpace( detail.DarkCover ) ) { 73 | API.RequestImage( detail.DarkCover, success, requestDefault ); 74 | } 75 | else { 76 | requestDefault(); 77 | } 78 | }, failure: requestDefault ); 79 | } 80 | else { 81 | requestDefault(); 82 | } 83 | } 84 | 85 | public override string ToString () 86 | => Source is Source.Web ? $"User with ID = {ID}" : $"Local user"; 87 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/API/BeatmapRecommendation.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #nullable disable 4 | namespace osu.Game.Rulesets.RurusettoAddon.API; 5 | 6 | public record BeatmapRecommendation { 7 | /// user_detail of user who recommend this beatmap. 8 | [JsonProperty( "user_detail" )] 9 | public UserDetail Recommender { get; init; } 10 | 11 | /// ID of this beatmap in osu!. 12 | [JsonProperty( "beatmap_id" )] 13 | public int BeatmapID { get; init; } 14 | 15 | /// ID of set of this beatmap in osu!. 16 | [JsonProperty( "beatmapset_id" )] 17 | public int BeatmapSetID { get; init; } 18 | 19 | /// Beatmap's song name. 20 | [JsonProperty( "title" )] 21 | public string Title { get; init; } 22 | 23 | /// Song's artist of this beatmap. 24 | [JsonProperty( "artist" )] 25 | public string Artist { get; init; } 26 | 27 | /// Song's source of this beatmap. 28 | [JsonProperty( "source" )] 29 | public string Source { get; init; } 30 | 31 | /// Name of user in osu! who create this beatmap (mapper). 32 | [JsonProperty( "creator" )] 33 | public string Creator { get; init; } 34 | 35 | /// Approval state of this beatmap (4 = loved, 3 = qualified, 2 = approved, 1 = ranked, 0 = pending, -1 = WIP, -2 = graveyard) 36 | [JsonProperty( "approved" )] 37 | public BeatmapStatus Status { get; init; } 38 | 39 | /// Star rating of this beatmap in osu! mode. 40 | [JsonProperty( "difficultyrating" )] 41 | public float StarDifficulty { get; init; } 42 | 43 | /// BPM of the song in this beatmap. 44 | [JsonProperty( "bpm" )] 45 | public float BPM { get; init; } 46 | 47 | /// Difficulty name of this beatmap in beatmap's beatmapset. 48 | [JsonProperty( "version" )] 49 | public string Version { get; init; } 50 | 51 | /// URL to go to this beatmap in osu! website. 52 | [JsonProperty( "url" )] 53 | public string Url { get; init; } 54 | 55 | /// URL of beatmap's cover image that use as the background in beatmap page. 56 | [JsonProperty( "beatmap_cover" )] 57 | public string CoverImage { get; init; } 58 | 59 | /// URL of beatmap's thumbnail image that use in old osu! site and in osu! stable. 60 | [JsonProperty( "beatmap_thumbnail" )] 61 | public string ThumbnailImage { get; init; } 62 | 63 | /// URL of beatmap's card image that use in new osu! new beatmap card design. 64 | [JsonProperty( "beatmap_card" )] 65 | public string CardImage { get; init; } 66 | 67 | /// URL of beatmap's list image that use in new osu! new beatmap card design. 68 | [JsonProperty( "beatmap_list" )] 69 | public string ListImage { get; init; } 70 | 71 | /// Comment from user who recommend this beatmap. 72 | [JsonProperty( "comment" )] 73 | public string Comment { get; init; } 74 | 75 | /// The time on this recommend beatmap added to the site in JSON time format. 76 | [JsonProperty( "created_at" )] 77 | public DateTime? CreatedAt { get; init; } 78 | } 79 | 80 | public enum BeatmapStatus { 81 | Pending = 0, 82 | Ranked = 1, 83 | Approved = 2, 84 | Qualified = 3, 85 | Loved = 4, 86 | WIP = -1, 87 | Graveyard = -2 88 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/API/ListingEntry.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #nullable disable 4 | namespace osu.Game.Rulesets.RurusettoAddon.API; 5 | 6 | public record ShortListingEntry { 7 | /// The ID of the ruleset in Rūrusetto database. 8 | [JsonProperty( "id" )] 9 | public int ID { get; init; } 10 | 11 | /// The name of the ruleset. 12 | [JsonProperty( "name" )] 13 | public string Name { get; init; } 14 | 15 | /// The slug of the ruleset. Use in the URL of the ruleset's wiki page. 16 | [JsonProperty( "slug" )] 17 | public string Slug { get; init; } 18 | 19 | /// The short description of the rulesets. 20 | [JsonProperty( "description" )] 21 | public string Description { get; init; } 22 | 23 | /// The URL of the ruleset icon that use in website's default theme (dark theme). 24 | [JsonProperty( "icon" )] 25 | public string DarkIcon { get; init; } 26 | 27 | /// The URL of the ruleset icon that use in website's light theme. 28 | [JsonProperty( "light_icon" )] 29 | public string LightIcon { get; init; } 30 | 31 | /// True if the wiki maintainer has verified that the the owner is the real owner of this ruleset. 32 | [JsonProperty( "verified" )] 33 | public bool IsVerified { get; init; } 34 | 35 | /// True if the rulesets is stop update or archived by rulesets creator. 36 | [JsonProperty( "archive" )] 37 | public bool IsArchived { get; init; } 38 | 39 | /// URL for download the latest release of ruleset from GitHub 40 | [JsonProperty( "direct_download_link" )] 41 | public string Download { get; init; } 42 | 43 | /// 44 | /// True if website can render the direct download link from the source and github_download_filename 45 | /// so user can download directly from direct_download_link. 46 | /// 47 | [JsonProperty( "can_download" )] 48 | public bool CanDownload { get; init; } 49 | 50 | /// The status of the ruleset. 51 | [JsonProperty( "status" )] 52 | public Status Status { get; init; } 53 | } 54 | 55 | public record ListingEntry : ShortListingEntry { 56 | /// The user_detail of the ruleset's current owner. 57 | [JsonProperty( "owner_detail" )] 58 | public UserDetail Owner { get; init; } 59 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/API/RulesetDetail.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Serialization; 3 | using osu.Framework.Localisation; 4 | 5 | #nullable disable 6 | namespace osu.Game.Rulesets.RurusettoAddon.API; 7 | 8 | public record RulesetDetail { 9 | /// 10 | /// Whether the request has succeded 11 | /// 12 | public bool Success => string.IsNullOrWhiteSpace( Detail ); 13 | 14 | /// The ID of the ruleset in Rūrusetto database. 15 | [JsonProperty( "id" )] 16 | public int ID { get; init; } 17 | 18 | /// The name of the ruleset. 19 | [JsonProperty( "name" )] 20 | public LocalisableString Name { get; init; } 21 | /// The slug of the ruleset. Use in the URL of the ruleset's wiki page. 22 | [JsonProperty( "slug" )] 23 | public string Slug { get; init; } 24 | /// The short description of the rulesets. 25 | [JsonProperty( "description" )] 26 | public LocalisableString Description { get; init; } 27 | 28 | /// The URL of the ruleset icon that use in website's default theme (dark theme). 29 | [JsonProperty( "icon" )] 30 | public string DarkIcon { get; init; } 31 | /// The URL of the ruleset icon that use in website's light theme. 32 | [JsonProperty( "light_icon" )] 33 | public string LightIcon { get; init; } 34 | /// The URL of the ruleset logo that use in the infobox. 35 | [JsonProperty( "logo" )] 36 | public string Logo { get; init; } 37 | /// The URL of the cover image in ruleset's wiki page in website's default theme (dark theme). 38 | [JsonProperty( "cover_image" )] 39 | public string CoverDark { get; init; } 40 | /// The URL of the cover image in ruleset's wiki page in website's light theme. 41 | [JsonProperty( "cover_image_light" )] 42 | public string CoverLight { get; init; } 43 | /// The URL of the image that use in the opengraph part of the wiki URL. 44 | [JsonProperty( "opengraph_image" )] 45 | public string OpengraphImage { get; init; } 46 | 47 | /// The URL of the CSS file that's override the website's default styling. 48 | [JsonProperty( "custom_css" )] 49 | public string CustomCSS { get; init; } 50 | /// Wiki main content in markdown format. 51 | [JsonProperty( "content" )] 52 | public LocalisableString Content { get; init; } 53 | 54 | /// The URL source of the rulesets. 55 | [JsonProperty( "source" )] 56 | public string Source { get; init; } 57 | /// Filename that use in rendering the direct download link with the source link. 58 | [JsonProperty( "github_download_filename" )] 59 | public string GithubFilename { get; init; } 60 | /// URL for download the latest release of ruleset from GitHub 61 | [JsonProperty( "direct_download_link" )] 62 | public string Download { get; init; } 63 | /// 64 | /// True if website can render the direct download link from the source and github_download_filename 65 | /// so user can download directly from direct_download_link. 66 | /// 67 | [JsonProperty( "can_download" )] 68 | public bool CanDownload { get; init; } 69 | 70 | /// The user_detail of the ruleset's current owner 71 | [JsonProperty( "owner_detail" )] 72 | public UserDetail Owner { get; init; } 73 | 74 | /// The user_detail of the user who create this wiki page, not the owner. 75 | [JsonProperty( "creator_detail" )] 76 | public UserDetail Creator { get; init; } 77 | /// The UTC time that the wiki page has create in JSON time format. 78 | [JsonProperty( "created_at" )] 79 | public DateTime? CreatedAt { get; init; } 80 | 81 | /// The user_detail of the user who edit the wiki page last time. 82 | [JsonProperty( "last_edited_by_detail" )] 83 | public UserDetail LastEditedBy { get; init; } 84 | /// The UTC time of the latest wiki edit. 85 | [JsonProperty( "last_edited_at" )] 86 | public DateTime? LastEditedAt { get; init; } 87 | 88 | /// True if the wiki maintainer has verified that the the owner is the real owner of this ruleset. 89 | [JsonProperty( "verified" )] 90 | public bool IsVerified { get; init; } 91 | /// True if this ruleset is stop update or archived by rulesets creator. 92 | [JsonProperty( "archive" )] 93 | public bool IsArchived { get; init; } 94 | 95 | [JsonProperty( "detail" )] 96 | private string Detail { get; init; } 97 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/API/Status.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #nullable disable 4 | namespace osu.Game.Rulesets.RurusettoAddon.API; 5 | 6 | public record Status { 7 | /// The latest version name of the ruleset. 8 | [JsonProperty( "latest_version" )] 9 | public string LatestVersion { get; init; } 10 | /// The time on ruleset's latest update in JSON time format. 11 | [JsonProperty( "latest_update" )] 12 | public DateTime? LatestUpdate { get; init; } 13 | 14 | /// True if the ruleset is marked as pre-release in GitHub Release. 15 | [JsonProperty( "pre_realase" )] 16 | public bool IsPrerelease { get; init; } 17 | /// The latest changelog of the ruleset in markdown format. 18 | [JsonProperty( "changelog" )] 19 | public string Changelog { get; init; } 20 | /// The size of the latest release file in bytes. 21 | [JsonProperty( "file_size" )] 22 | public int FileSize { get; init; } 23 | /// The status about the playable of the ruleset. Has 3 choices (yes, no, unknown) 24 | [JsonProperty( "playable" )] 25 | public string PlayableStatus { get; init; } 26 | 27 | public bool IsPlayable => PlayableStatus == "yes"; 28 | public bool IsBorked => PlayableStatus == "no"; 29 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/API/Subpage.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #nullable disable 4 | namespace osu.Game.Rulesets.RurusettoAddon.API; 5 | 6 | public record Subpage { 7 | /// Details of ruleset. 8 | [JsonProperty( "ruleset_detail" )] 9 | public RulesetDetail RulesetDetail { get; init; } 10 | /// Title of the subpage 11 | [JsonProperty( "title" )] 12 | public string Title { get; init; } 13 | /// Slug of the subpage. Use in subpage URL path. 14 | [JsonProperty( "slug" )] 15 | public string Slug { get; init; } 16 | /// Content of the subpage in markdown format. 17 | [JsonProperty( "content" )] 18 | public string Content { get; init; } 19 | /// user_detail of user who create this page. 20 | [JsonProperty( "creator_detail" )] 21 | public UserDetail Creator { get; init; } 22 | /// user_detail of user who last edited this subpage. 23 | [JsonProperty( "last_edited_by_detail" )] 24 | public UserDetail LastEditedBy { get; init; } 25 | /// The UTC time of the latest wiki edit in JSON time format. 26 | [JsonProperty( "last_edited_at" )] 27 | public DateTime? LastEditedAt { get; init; } 28 | /// The UTC time that the wiki page has create in JSON time format. 29 | [JsonProperty( "created_at" )] 30 | public DateTime? CreatedAt { get; init; } 31 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/API/SubpageListingEntry.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using osu.Framework.Localisation; 3 | 4 | #nullable disable 5 | namespace osu.Game.Rulesets.RurusettoAddon.API; 6 | 7 | public record SubpageListingEntry { 8 | /// Title of the subpage 9 | [JsonProperty( "title" )] 10 | public LocalisableString Title { get; init; } 11 | 12 | /// Slug of the subpage. Use in subpage URL path. 13 | [JsonProperty( "slug" )] 14 | public string Slug { get; init; } 15 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/API/UserDetail.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #nullable disable 4 | namespace osu.Game.Rulesets.RurusettoAddon.API; 5 | 6 | public record UserDetail { 7 | /// The ID of the user in Rūrusetto database. 8 | [JsonProperty( "id" )] 9 | public int? ID { get; init; } 10 | 11 | [JsonProperty( "user" )] 12 | private UserInfo info { get; init; } 13 | 14 | /// 15 | public string Username => info.Username; 16 | 17 | /// 18 | public string Email => info.Email; 19 | 20 | /// The URL of the user's profile image. 21 | [JsonProperty( "image" )] 22 | public string ProfilePicture { get; init; } 23 | } 24 | 25 | public record UserInfo { 26 | /// Username of request user. 27 | [JsonProperty( "username" )] 28 | public string Username { get; init; } 29 | 30 | /// Email of request user. (Can be blank) 31 | [JsonProperty( "email" )] 32 | public string Email { get; init; } 33 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/API/UserProfile.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | #nullable disable 4 | namespace osu.Game.Rulesets.RurusettoAddon.API; 5 | 6 | public record UserProfile { 7 | /// The ID of the user. Use in URL path to target user's profile page. 8 | [JsonProperty( "id" )] 9 | public int? ID { get; init; } 10 | 11 | [JsonProperty( "user" )] 12 | private UserInfo info { get; init; } 13 | 14 | /// URL of the user's profile picture. 15 | [JsonProperty( "image" )] 16 | public string ProfilePicture { get; init; } 17 | 18 | /// URL of the user's cover picture in website's default theme (Dark theme). 19 | [JsonProperty( "cover" )] 20 | public string DarkCover { get; init; } 21 | 22 | /// URL of the user's cover picture in website's light theme. 23 | [JsonProperty( "cover_light" )] 24 | public string LightCover { get; init; } 25 | 26 | /// User's introduction text on profile page. 27 | [JsonProperty( "about_me" )] 28 | public string Bio { get; init; } 29 | 30 | /// osu! account username of target user (Can be blank) 31 | [JsonProperty( "osu_username" )] 32 | public string OsuUsername { get; init; } 33 | 34 | /// 35 | public string Username => info?.Username; 36 | 37 | /// 38 | public string Email => info?.Email; 39 | 40 | /// List of tag that user has. Will be [] if no tags found in this user. 41 | [JsonProperty( "tags" )] 42 | public List Tags { get; init; } 43 | 44 | /// List of ruleset that user created. Will be [] if no created rulesets found from this user. 45 | [JsonProperty( "created_rulesets" )] 46 | public List CreatedRulesets { get; init; } 47 | } 48 | 49 | public record UserTag { 50 | /// The name of the tag. 51 | [JsonProperty( "name" )] 52 | public string Text { get; init; } 53 | 54 | /// The background color of the tag pills that show in profile. Will return in hex color (e.g. #FFFFFF). 55 | [JsonProperty( "pills_color" )] 56 | public string BackgroundColor { get; init; } 57 | 58 | /// The font color of the tag pills that show in profile. Will return in hex color (e.g. #FFFFFF). 59 | [JsonProperty( "font_color" )] 60 | public string ForegroundColor { get; init; } 61 | 62 | /// The description of the tag. 63 | [JsonProperty( "description" )] 64 | public string Description { get; init; } 65 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/APIRulesetStore.cs: -------------------------------------------------------------------------------- 1 | using Humanizer; 2 | using osu.Framework.Platform; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | 6 | namespace osu.Game.Rulesets.RurusettoAddon; 7 | 8 | public class APIRulesetStore { 9 | public Storage? Storage { get; init; } 10 | public IRulesetStore? RulesetStore { get; init; } 11 | public RurusettoAPI? API { get; init; } 12 | 13 | List cachedIdentities = new(); 14 | Task? identities; 15 | public Task RequestIdentities () { 16 | if ( identities is null ) 17 | identities = getIdentities(); 18 | 19 | return identities; 20 | } 21 | 22 | public void Refresh () => identities = null; 23 | 24 | async Task getIdentities () { 25 | var (newIdentities, hasLocal, hasWeb) = await fetchIdentities(); 26 | 27 | // we need to merge them since we dont want anything to get out of sync, like the download manager which uses a given APIRuleset instance 28 | lock ( cachedIdentities ) { 29 | for ( int i = 0; i < newIdentities.Count; i++ ) { 30 | var ruleset = newIdentities[i]; 31 | 32 | var match = cachedIdentities.FirstOrDefault( x => 33 | ( x.Source is Source.Web && ruleset.Source is Source.Web && x.Slug == ruleset.Slug ) || 34 | ( x.Source is Source.Local && ruleset.Source is Source.Local && x.LocalPath == ruleset.LocalPath ) || 35 | ( x.Source is Source.Local && ruleset.Source is Source.Web && Path.GetFileName( x.LocalPath ) == Path.GetFileName( ruleset.ListingEntry?.Download ) ) || 36 | ( x.Source is Source.Web && ruleset.Source is Source.Local && Path.GetFileName( ruleset.LocalPath ) == Path.GetFileName( x.ListingEntry?.Download ) ) 37 | ); 38 | 39 | if ( match != null ) { 40 | match.Merge( ruleset ); 41 | newIdentities[i] = match; 42 | } 43 | else { 44 | cachedIdentities.Add( ruleset ); 45 | } 46 | } 47 | } 48 | 49 | return new( newIdentities ) { 50 | ContainsWebListing = hasWeb, 51 | ContainsLocalListing = hasLocal 52 | }; 53 | } 54 | 55 | async Task<(List rulesets, bool hasLocal, bool hasWeb)> fetchIdentities () { 56 | List identities = new(); 57 | 58 | Dictionary webFilenames = new(); 59 | Dictionary webNames = new(); 60 | 61 | bool hasLocal = Storage != null; 62 | bool hasWeb = false; 63 | 64 | if ( API != null ) { 65 | IEnumerable listing = Array.Empty(); 66 | var task = new TaskCompletionSource(); 67 | 68 | API.RequestRulesetListing( result => { 69 | listing = result; 70 | hasWeb = true; 71 | task.SetResult(); 72 | 73 | }, failure: e => { 74 | API.LogFailure( $"API ruleset store could not retrieve ruleset listing", e ); 75 | task.SetResult(); 76 | }, 77 | cancelled: () => task.SetResult() 78 | ); 79 | 80 | await task.Task; 81 | 82 | foreach ( var entry in listing ) { 83 | APIRuleset id; 84 | identities.Add( id = new() { 85 | Source = Source.Web, 86 | API = API, 87 | Name = entry.Name, 88 | Slug = entry.Slug, 89 | ListingEntry = entry, 90 | IsModifiable = true 91 | } ); 92 | 93 | var filename = Path.GetFileName( entry.Download ); 94 | if ( !string.IsNullOrWhiteSpace( filename ) ) { 95 | // there shouldnt be multiple, but if there are we dont want to crash 96 | // TODO we might want to report if this ever happens 97 | webFilenames.TryAdd( filename, id ); 98 | } 99 | webNames.TryAdd( entry.Name.Humanize().ToLower(), id ); 100 | } 101 | } 102 | 103 | if ( RulesetStore != null ) { 104 | Dictionary localPaths = new(); 105 | var imported = RulesetStore.AvailableRulesets; 106 | 107 | foreach ( var ruleset in imported ) { 108 | try { 109 | var instance = ruleset.CreateInstance(); 110 | if ( instance is null ) { 111 | // TODO report this 112 | continue; 113 | } 114 | 115 | var path = instance.GetType().Assembly.Location; 116 | var filename = Path.GetFileName( path ); 117 | if ( string.IsNullOrWhiteSpace( filename ) ) { 118 | // TODO use type name as filename and if a file with that name isnt found report this 119 | filename = ""; 120 | } 121 | 122 | if ( webFilenames.TryGetValue( filename, out var id ) || webNames.TryGetValue( ruleset.Name.Humanize().ToLower(), out id ) ) { 123 | id.IsPresentLocally = true; 124 | id.LocalPath = path; 125 | id.LocalRulesetInfo = ruleset; 126 | id.Name = ruleset.Name; 127 | id.ShortName = ruleset.ShortName; 128 | id.IsModifiable = Storage != null && path.StartsWith( Storage.GetFullPath( "./rulesets" ), StringComparison.Ordinal ); 129 | } 130 | else { 131 | identities.Add( id = new() { 132 | Source = Source.Local, 133 | API = API, 134 | IsPresentLocally = true, 135 | LocalPath = path, 136 | LocalRulesetInfo = ruleset, 137 | Name = ruleset.Name, 138 | ShortName = ruleset.ShortName, 139 | IsModifiable = Storage != null && path.StartsWith( Storage.GetFullPath( "./rulesets" ), StringComparison.Ordinal ) 140 | } ); 141 | } 142 | 143 | localPaths.Add( path, id ); 144 | } 145 | catch ( Exception ) { 146 | // TODO report this 147 | } 148 | } 149 | 150 | if ( Storage != null ) { 151 | foreach ( var path in Storage.GetFiles( "./rulesets", "osu.Game.Rulesets.*.dll" ) ) { 152 | if ( localPaths.TryGetValue( Storage.GetFullPath( path ), out var id ) ) { 153 | // we already know its there then 154 | } 155 | else if ( webFilenames.TryGetValue( Path.GetFileName( path ), out id ) ) { 156 | id.IsPresentLocally = true; 157 | id.IsModifiable = true; 158 | id.LocalPath = Storage.GetFullPath( path ); 159 | id.HasImportFailed = true; 160 | } 161 | else { 162 | identities.Add( new() { 163 | Source = Source.Local, 164 | API = API, 165 | Name = Path.GetFileName( path ).Split( '.' ).SkipLast( 1 ).Last(), 166 | IsPresentLocally = true, 167 | HasImportFailed = true, 168 | LocalPath = Storage.GetFullPath( path ), 169 | IsModifiable = true 170 | } ); 171 | } 172 | } 173 | } 174 | } 175 | else if ( Storage != null ) { 176 | foreach ( var path in Storage.GetFiles( "./rulesets", "osu.Game.Rulesets.*.dll" ) ) { 177 | if ( webFilenames.TryGetValue( Path.GetFileName( path ), out var id ) ) { 178 | id.IsPresentLocally = true; 179 | id.LocalPath = Storage.GetFullPath( path ); 180 | id.IsModifiable = true; 181 | } 182 | else { 183 | identities.Add( new() { 184 | Source = Source.Local, 185 | API = API, 186 | Name = Path.GetFileName( path ).Split( '.' ).SkipLast( 1 ).Last(), 187 | IsPresentLocally = true, 188 | LocalPath = Storage.GetFullPath( path ), 189 | IsModifiable = true 190 | } ); 191 | } 192 | } 193 | } 194 | 195 | return (identities, hasLocal, hasWeb); 196 | } 197 | } 198 | 199 | public record RulesetIdentities ( IEnumerable Rulesets ) : IEnumerable { 200 | public bool ContainsWebListing { get; init; } 201 | public bool ContainsLocalListing { get; init; } 202 | 203 | public IEnumerator GetEnumerator () { 204 | return Rulesets.GetEnumerator(); 205 | } 206 | 207 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () { 208 | return ( (System.Collections.IEnumerable)Rulesets ).GetEnumerator(); 209 | } 210 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/APIUserStore.cs: -------------------------------------------------------------------------------- 1 | namespace osu.Game.Rulesets.RurusettoAddon; 2 | 3 | public class APIUserStore { 4 | RurusettoAPI API; 5 | APIUser unknownUser; 6 | Dictionary users = new(); 7 | public APIUserStore ( RurusettoAPI API ) { 8 | this.API = API; 9 | unknownUser = APIUser.Local( API ); 10 | } 11 | 12 | public APIUser GetUser ( int id ) 13 | => users.GetOrAdd( id, () => APIUser.FromID( API, id ) ); 14 | 15 | public APIUser GetUser ( UserDetail? detail ) 16 | => detail?.ID is int id ? GetUser( id ) : unknownUser; 17 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/Configuration/RurusettoConfigManager.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Configuration; 2 | using osu.Game.Rulesets.Configuration; 3 | 4 | namespace osu.Game.Rulesets.RurusettoAddon.Configuration; 5 | 6 | public class RurusettoConfigManager : RulesetConfigManager { 7 | public RurusettoConfigManager ( SettingsStore store, RulesetInfo ruleset, int? variant = null ) : base( store, ruleset, variant ) { 8 | AddBindable( RurusettoSetting.APIAddress, new Bindable( RurusettoAPI.DefaultAPIAddress ) ); 9 | } 10 | } 11 | 12 | public enum RurusettoSetting { 13 | APIAddress 14 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/Extensions.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using System.Linq; 3 | global using System.Collections.Generic; 4 | global using osuTK; 5 | global using osu.Framework.Bindables; 6 | global using osu.Framework.Graphics; 7 | global using osu.Framework.Allocation; 8 | global using osu.Framework.Graphics.Containers; 9 | global using osu.Framework.Graphics.Shapes; 10 | global using osu.Framework.Graphics.Sprites; 11 | global using osu.Game.Graphics; 12 | global using osu.Game.Graphics.Containers; 13 | global using osu.Game.Rulesets.RurusettoAddon.API; 14 | global using osu.Game.Rulesets.RurusettoAddon.UI.Overlay; 15 | global using osu.Framework.Extensions.Color4Extensions; 16 | global using osu.Framework.Graphics.Colour; 17 | global using osu.Framework.Graphics.Primitives; 18 | global using Image = SixLabors.ImageSharp.Image; 19 | using System.Reflection; 20 | 21 | namespace osu.Game.Rulesets.RurusettoAddon; 22 | 23 | public static class Extensions { 24 | public static T GetOrAdd ( this IDictionary self, Tkey key, Func @default ) { 25 | if ( !self.TryGetValue( key, out var value ) ) 26 | self.Add( key, value = @default() ); 27 | 28 | return value; 29 | } 30 | 31 | public static T GetOrAdd ( this IDictionary self, Tkey key, Func @default, Action after ) { 32 | if ( !self.TryGetValue( key, out var value ) ) { 33 | self.Add( key, value = @default() ); 34 | after( value ); 35 | } 36 | 37 | return value; 38 | } 39 | 40 | public static T? GetField ( this (Type type, object instance) self, string name ) { 41 | return (T?)self.type.GetField( name, BindingFlags.NonPublic | BindingFlags.Instance )?.GetValue( self.instance ); 42 | } 43 | 44 | public static MethodInfo? GetMethod ( this (Type type, object instance) self, string name ) { 45 | return self.type.GetMethod( name, BindingFlags.NonPublic | BindingFlags.Instance )?.MakeGenericMethod( typeof( T1 ) ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/Localisation/Strings.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Localisation; 2 | 3 | namespace osu.Game.Rulesets.RurusettoAddon.Localisation; 4 | 5 | public static class Strings { 6 | private const string prefix = "osu.Game.Rulesets.RurusettoAddon.Localisation.Strings"; 7 | 8 | /// 9 | /// Local ruleset, not listed on the wiki. 10 | /// 11 | public static LocalisableString LocalRulesetDescription => new TranslatableString( getKey( "local-ruleset-description" ), "Local ruleset, not listed on the wiki." ); 12 | 13 | /// 14 | /// Failed to fetch the ruleset wiki page. Sorry! 15 | /// You can still try visiting it at [rurusetto]({0}). 16 | /// 17 | public static LocalisableString PageLoadFailed ( LocalisableString wikiLink ) 18 | => new TranslatableString( getKey( "page-load-failed" ), "Failed to fetch the ruleset wiki page. Sorry!\nYou can still try visiting it at [rurusetto]({0}).", wikiLink ); 19 | 20 | /// 21 | /// listing 22 | /// 23 | public static LocalisableString ListingTab => new TranslatableString( getKey( "listing-tab" ), "listing" ); 24 | 25 | /// 26 | /// users 27 | /// 28 | public static LocalisableString UsersTab => new TranslatableString( getKey( "users-tab" ), "users" ); 29 | 30 | /// 31 | /// collections 32 | /// 33 | public static LocalisableString CollectionsTab => new TranslatableString( getKey( "collections-tab" ), "collections" ); 34 | 35 | /// 36 | /// browse and manage rulesets 37 | /// 38 | public static LocalisableString RurusettoDescription => new TranslatableString( getKey( "rurusetto-description" ), "browse and manage rulesets" ); 39 | 40 | /// 41 | /// Unknown 42 | /// 43 | public static LocalisableString UserUnknown => new TranslatableString( getKey( "user-unknown" ), "Unknown" ); 44 | 45 | /// 46 | /// ARCHIVED 47 | /// 48 | public static LocalisableString TagArchived => new TranslatableString( getKey( "tag-archived" ), "ARCHIVED" ); 49 | 50 | /// 51 | /// This ruleset is no longer maintained 52 | /// 53 | public static LocalisableString TagArchivedTooltip => new TranslatableString( getKey( "tag-archived-tooltip" ), "This ruleset is no longer maintained" ); 54 | 55 | /// 56 | /// UNLISTED 57 | /// 58 | public static LocalisableString TagLocal => new TranslatableString( getKey( "tag-local" ), "UNLISTED" ); 59 | 60 | /// 61 | /// This ruleset is installed locally, but is not listed on the wiki 62 | /// 63 | public static LocalisableString TagLocalTooltip => new TranslatableString( getKey( "tag-local-tooltip" ), "This ruleset is installed locally, but is not listed on the wiki" ); 64 | 65 | /// 66 | /// HARD CODED 67 | /// 68 | public static LocalisableString TagHardcoded => new TranslatableString( getKey( "tag-hardcoded" ), "HARD CODED" ); 69 | 70 | /// 71 | /// This ruleset is hard coded into the game and cannot be modified 72 | /// 73 | public static LocalisableString TagHardcodedTooltip => new TranslatableString( getKey( "tag-hardcoded-tooltip" ), "This ruleset is hard coded into the game and cannot be modified" ); 74 | 75 | /// 76 | /// FAILED IMPORT 77 | /// 78 | public static LocalisableString TagFailedImport => new TranslatableString( getKey( "tag-failed-import" ), "FAILED IMPORT" ); 79 | 80 | /// 81 | /// This ruleset is downloaded, but failed to import 82 | /// 83 | public static LocalisableString TagFailedImportTooltip => new TranslatableString( getKey( "tag-failed-import-tooltip" ), "This ruleset is downloaded, but failed to import" ); 84 | 85 | /// 86 | /// BORKED 87 | /// 88 | public static LocalisableString TagBorked => new TranslatableString( getKey( "tag-borked" ), "BORKED" ); 89 | 90 | /// 91 | /// This ruleset does not work 92 | /// 93 | public static LocalisableString TagBorkedTooltip => new TranslatableString( getKey( "tag-borked-tooltip" ), "This ruleset does not work" ); 94 | 95 | /// 96 | /// PLAYABLE 97 | /// 98 | public static LocalisableString TagPlayable => new TranslatableString( getKey( "tag-playable" ), "PLAYABLE" ); 99 | 100 | /// 101 | /// This ruleset works 102 | /// 103 | public static LocalisableString TagPlayableTooltip => new TranslatableString( getKey( "tag-playable-tooltip" ), "This ruleset works" ); 104 | 105 | /// 106 | /// PRE-RELEASE 107 | /// 108 | public static LocalisableString TagPrerelease => new TranslatableString( getKey( "tag-prerelease" ), "PRE-RELEASE" ); 109 | 110 | /// 111 | /// The current version is a pre-release 112 | /// 113 | public static LocalisableString TagPrereleaseTooltip => new TranslatableString( getKey( "tag-prerelease-tooltip" ), "The current version is a pre-release" ); 114 | 115 | /// 116 | /// Home Page 117 | /// 118 | public static LocalisableString HomePage => new TranslatableString( getKey( "home-page" ), "Home Page" ); 119 | 120 | /// 121 | /// Report Issue 122 | /// 123 | public static LocalisableString ReportIssue => new TranslatableString( getKey( "report-issue" ), "Report Issue" ); 124 | 125 | /// 126 | /// Checking... 127 | /// 128 | public static LocalisableString DownloadChecking => new TranslatableString( getKey( "download-checking" ), "Checking..." ); 129 | 130 | /// 131 | /// Unavailable Online 132 | /// 133 | public static LocalisableString UnavailableOnline => new TranslatableString( getKey( "unavailable-online" ), "Unavailable Online" ); 134 | 135 | /// 136 | /// Installed, not available online 137 | /// 138 | public static LocalisableString InstalledUnavailableOnline => new TranslatableString( getKey( "installed-unavailable-online" ), "Installed, not available online" ); 139 | 140 | /// 141 | /// Download 142 | /// 143 | public static LocalisableString Download => new TranslatableString( getKey( "download" ), "Download" ); 144 | 145 | /// 146 | /// Re-download 147 | /// 148 | public static LocalisableString Redownload => new TranslatableString( getKey( "redownload" ), "Re-download" ); 149 | 150 | /// 151 | /// Downloading... 152 | /// 153 | public static LocalisableString Downloading => new TranslatableString( getKey( "downloading" ), "Downloading..." ); 154 | 155 | /// 156 | /// Update 157 | /// 158 | public static LocalisableString Update => new TranslatableString( getKey( "update" ), "Update" ); 159 | 160 | /// 161 | /// Remove 162 | /// 163 | public static LocalisableString Remove => new TranslatableString( getKey( "remove" ), "Remove" ); 164 | 165 | /// 166 | /// Cancel Download 167 | /// 168 | public static LocalisableString CancelDownload => new TranslatableString( getKey( "cancel-download" ), "Cancel Download" ); 169 | 170 | /// 171 | /// Cancel Removal 172 | /// 173 | public static LocalisableString CancelRemove => new TranslatableString( getKey( "cancel-remove" ), "Cancel Removal" ); 174 | 175 | /// 176 | /// Cancel Update 177 | /// 178 | public static LocalisableString CancelUpdate => new TranslatableString( getKey( "cancel-update" ), "Cancel Update" ); 179 | 180 | /// 181 | /// Refresh 182 | /// 183 | public static LocalisableString Refresh => new TranslatableString( getKey( "refresh" ), "Refresh" ); 184 | 185 | /// 186 | /// Will be removed on restart! 187 | /// 188 | public static LocalisableString ToBeRemoved => new TranslatableString( getKey( "to-be-removed" ), "Will be removed on restart!" ); 189 | 190 | /// 191 | /// Will be installed on restart! 192 | /// 193 | public static LocalisableString ToBeInstalled => new TranslatableString( getKey( "to-be-installed" ), "Will be installed on restart!" ); 194 | 195 | /// 196 | /// Will be updated on restart! 197 | /// 198 | public static LocalisableString ToBeUpdated => new TranslatableString( getKey( "to-be-updated" ), "Will be updated on restart!" ); 199 | 200 | /// 201 | /// Installed 202 | /// 203 | public static LocalisableString Installed => new TranslatableString( getKey( "installed" ), "Installed" ); 204 | 205 | /// 206 | /// Outdated 207 | /// 208 | public static LocalisableString Outdated => new TranslatableString( getKey( "outdated" ), "Outdated" ); 209 | 210 | /// 211 | /// Verified Ruleset Creator 212 | /// 213 | public static LocalisableString CreatorVerified => new TranslatableString( getKey( "creator-verified" ), "Verified Ruleset Creator" ); 214 | 215 | /// 216 | /// Could not load rurusetto-addon: Please report this to the rurusetto-addon repository NOT the osu!lazer repository: Code {0} 217 | /// 218 | public static LocalisableString LoadError ( LocalisableString errorCode ) 219 | => new TranslatableString( getKey( "load-error" ), "Could not load rurusetto-addon: Please report this to the rurusetto-addon repository NOT the osu!lazer repository: Code {0}", errorCode ); 220 | 221 | /// 222 | /// Main 223 | /// 224 | public static LocalisableString MainPage => new TranslatableString( getKey( "main-page" ), "Main" ); 225 | 226 | /// 227 | /// Changelog 228 | /// 229 | public static LocalisableString ChangelogPage => new TranslatableString( getKey( "changelog-page" ), "Changelog" ); 230 | 231 | /// 232 | /// Recommended Beatmaps 233 | /// 234 | public static LocalisableString RecommendedBeatmapsPage => new TranslatableString( getKey( "recommended-beatmaps-page" ), "Recommended Beatmaps" ); 235 | 236 | /// 237 | /// Unknown Version 238 | /// 239 | public static LocalisableString UnknownVersion => new TranslatableString( getKey( "unknown-version" ), "Unknown Version" ); 240 | 241 | /// 242 | /// Rurusetto Addon 243 | /// 244 | public static LocalisableString SettingsHeader => new TranslatableString( getKey( "settings-header" ), "Rurusetto Addon" ); 245 | 246 | /// 247 | /// API Address 248 | /// 249 | public static LocalisableString SettingsApiAddress => new TranslatableString( getKey( "settings-api-address" ), "API Address" ); 250 | 251 | /// 252 | /// Untitled Ruleset 253 | /// 254 | public static LocalisableString UntitledRuleset => new TranslatableString( getKey( "untitled-ruleset" ), "Untitled Ruleset" ); 255 | 256 | /// 257 | /// Oh no! 258 | /// 259 | public static LocalisableString ErrorHeader => new TranslatableString( getKey( "error-header" ), "Oh no!" ); 260 | 261 | /// 262 | /// Please make sure you have an internet connection and the API address in settings is correct 263 | /// 264 | public static LocalisableString ErrorFooter => new TranslatableString( getKey( "error-footer" ), "Please make sure you have an internet connection and the API address in settings is correct" ); 265 | 266 | /// 267 | /// Retry 268 | /// 269 | public static LocalisableString Retry => new TranslatableString( getKey( "retry" ), "Retry" ); 270 | 271 | /// 272 | /// Could not retrieve the ruleset listing 273 | /// 274 | public static LocalisableString ListingFetchError => new TranslatableString( getKey( "listing-fetch-error" ), "Could not retrieve the ruleset listing" ); 275 | 276 | /// 277 | /// Could not load the page 278 | /// 279 | public static LocalisableString PageFetchError => new TranslatableString( getKey( "page-fetch-error" ), "Could not load the page" ); 280 | 281 | /// 282 | /// Could not retrieve subpages 283 | /// 284 | public static LocalisableString SubpagesFetchError => new TranslatableString( getKey( "subpages-fetch-error" ), "Could not retrieve subpages" ); 285 | 286 | /// 287 | /// Something went wrong, but I don't know what! 288 | /// 289 | public static LocalisableString ErrorMessageGeneric => new TranslatableString( getKey( "error-message-generic" ), "Something went wrong, but I don't know what!" ); 290 | 291 | /// 292 | /// It seems rurusetto addon couldn't finish some work. Please make sure all your changes were applied correctly 293 | /// 294 | public static LocalisableString NotificationWorkIncomplete => new TranslatableString( getKey( "notification-work-incomplete" ), "It seems rurusetto addon couldn't finish some work. Please make sure all your changes were applied correctly" ); 295 | 296 | private static string getKey ( string key ) => $"{prefix}:{key}"; 297 | } 298 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/Localisation/Strings.es.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | text/microsoft-resx1.3System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089Ruleset local, no listado en la wiki. 31 | No se pudo obtener la página de la wiki del ruleset. ¡Lo siento! 32 | Todavía puedes intentar visitarla en [rurusetto]({0}). 33 | listado 34 | usuarios 35 | colecciones 36 | explorar y administrar rulesets 37 | Desconocido 38 | ARCHIVADO 39 | Este ruleset ya no se mantiene 40 | NO LISTADO 41 | Este ruleset está instalado localmente, pero no está listado en la wiki 42 | CODIFICADO 43 | Este ruleset está codificado en el juego y no se puede modificar. 44 | IMPORTACIÓN FALLIDA 45 | Este ruleset se descargó, pero no se pudo importar 46 | BORRADO 47 | Este ruleset no funciona. 48 | JUGABLE 49 | Este ruleset funciona 50 | PRE-LANZAMIENTO 51 | La versión actual es un pre-lanzamiento. 52 | Página de inicio 53 | Reportar problema 54 | Comprobando... 55 | No disponible en línea 56 | Instalado, no disponible en línea 57 | Descargar 58 | Re-descargar 59 | Descargando... 60 | Actualizar 61 | Eliminar 62 | Cancelar descarga 63 | Cancelar eliminación 64 | Cancelar actualización 65 | Actualizar 66 | Se eliminará al reiniciar! 67 | Se instalará al reiniciar! 68 | Se actualizará al restart! 69 | Instalado 70 | Desactualizado 71 | Creador de rulesets verificado 72 | No se pudo cargar rurusetto-addon: Informe esto al repositorio de rurusetto-addon NO al repositorio de osu!lazer: Código {0} 73 | Principal 74 | Registro de cambios 75 | Beatmaps recomendados 76 | Versión desconocida 77 | Rurusetto Addon 78 | Dirección de la API 79 | Ruleset sin título 80 | Oh no! 81 | Asegúrese de tener una conexión a Internet y que la dirección de la API en la configuración sea correcta. 82 | Reintentar 83 | No se pudo recuperar el listado de rulesets 84 | No se pudo cargar la página 85 | No se pudieron recuperar las subpáginas 86 | Algo salió mal, pero no sé qué! 87 | Parece que rurusetto addon no pudo terminar un trabajo. Por favor, asegúrese de que todos sus cambios se aplicaron correctamente 88 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/Localisation/Strings.pl.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | text/microsoft-resx1.3System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089Lokalny tryb gry, nie wymieniony na wiki. 31 | Nie udało się pobrać strony o tym trybie gry. 32 | Jeśli chcesz, ciągle możesz spróbować na oficjalnej stronie [rurusetto]({0}). 33 | katalog 34 | przeglądaj i zarządzaj trybami gry 35 | Nieznany Użytkownik 36 | ARCHIWUM 37 | Ten tryb gry nie jest już utrzymywany 38 | LOKALNY 39 | Ten tryb gry jest zainstalowany lokalnie, ale nie ma go na wiki 40 | WBUDOWANY 41 | Ten tryb gry jest wbudowany w grę i nie da się go modyfikować 42 | BŁĄD IMPORTU 43 | Ten tryb gry jest pobrany, ale nie udało się go zaimportować 44 | ZDZBANIONY 45 | Ten tryb gry nie działa 46 | GRYWALNY 47 | Ten tryb gry działa 48 | PRE-RELEASE 49 | Obecna wersja tego trybu gry jest niedokończona 50 | Strona domowa 51 | Zgłoś problem 52 | Sprawdzanie... 53 | Niedostępny Online 54 | Zainstalowany, ale niedostępny online 55 | Pobierz 56 | Pobierz ponownie 57 | Pobieranie... 58 | Uaktualnij 59 | Usuń 60 | Anuluj Pobieranie 61 | Anuluj Usuwanie 62 | Anujuj Aktualizacje 63 | Odśwież 64 | Zostanie usunięty przy następnym uruchomieniu! 65 | Zostanie zainstalowany przy następnym uruchomieniu! 66 | Zostanie uaktualniony przy następnym uruchomieniu! 67 | Zainstalowany 68 | Przestarzały 69 | Zweryfikowani Twórcy Trybu 70 | Nie udało się uruchomić rurusetto-addon: Proszę, zgłoś to do repozytorium rurusetto-addon i NIE do repozytorium osu!lazer: Kod {0} 71 | Główna 72 | Zmiany 73 | Polecane Mapy 74 | Nieznana Wersja 75 | Zintegrowane Rurusetto 76 | Adres API 77 | Nienazwany Tryb Gry 78 | Ups! 79 | Upewnij się, że masz połączenie z internetem, a adres API w ustawieniach jest poprawny 80 | Odśwież 81 | Nie udało się pobrać katalogu 82 | Nie udało się załadować strony 83 | Nie udało się pobrać listy podstron 84 | Coś poszło nie tak, ale nie wiem co! 85 | Wygląda na to, że wtyczka rurusetto nie dokończyła pracy. Sprawdź, czy wszystkie twoje zmiany zostały pomyślnie zaaplikowane 86 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/Localisation/Strings.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | text/microsoft-resx1.3System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089Local ruleset, not listed on the wiki. 31 | Failed to fetch the ruleset wiki page. Sorry! 32 | You can still try visiting it at [rurusetto]({0}). 33 | listing 34 | users 35 | collections 36 | browse and manage rulesets 37 | Unknown 38 | ARCHIVED 39 | This ruleset is no longer maintained 40 | UNLISTED 41 | This ruleset is installed locally, but is not listed on the wiki 42 | HARD CODED 43 | This ruleset is hard coded into the game and cannot be modified 44 | FAILED IMPORT 45 | This ruleset is downloaded, but failed to import 46 | BORKED 47 | This ruleset does not work 48 | PLAYABLE 49 | This ruleset works 50 | PRE-RELEASE 51 | The current version is a pre-release 52 | Home Page 53 | Report Issue 54 | Checking... 55 | Unavailable Online 56 | Installed, not available online 57 | Download 58 | Re-download 59 | Downloading... 60 | Update 61 | Remove 62 | Cancel Download 63 | Cancel Removal 64 | Cancel Update 65 | Refresh 66 | Will be removed on restart! 67 | Will be installed on restart! 68 | Will be updated on restart! 69 | Installed 70 | Outdated 71 | Verified Ruleset Creator 72 | Could not load rurusetto-addon: Please report this to the rurusetto-addon repository NOT the osu!lazer repository: Code {0} 73 | Main 74 | Changelog 75 | Recommended Beatmaps 76 | Unknown Version 77 | Rurusetto Addon 78 | API Address 79 | Untitled Ruleset 80 | Oh no! 81 | Please make sure you have an internet connection and the API address in settings is correct 82 | Retry 83 | Could not retrieve the ruleset listing 84 | Could not load the page 85 | Could not retrieve subpages 86 | Something went wrong, but I don't know what! 87 | It seems rurusetto addon couldn't finish some work. Please make sure all your changes were applied correctly 88 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/Localisation/Strings.ru.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | text/microsoft-resx1.3System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089Локальный режим, которого нет на вики 31 | Не удалось загрузить страницу режима на вики. Извините! 32 | Попробуйте открыть ее на сайте [rurusetto]({1}). 33 | список 34 | открыть список доступных режимов 35 | Аноним 36 | В АРХИВЕ 37 | Этот режим больше не поддерживается 38 | ЛОКАЛЬНЫЙ 39 | Этот режим установлен только у вас, его нет на вики 40 | ВСТРОЕННЫЙ 41 | Этот режим встроен в игру и не может быть изменён 42 | ОШИБКА ИМПОРТА 43 | Режим скачан, но его не удалось импортировать 44 | НЕРАБОЧИЙ 45 | Этот режим не работает 46 | РАБОЧИЙ 47 | Этот режим работает 48 | БЕТА 49 | Текущая версия находится в стадии активной разработки 50 | Домашняя страница 51 | Сообщить о проблеме 52 | Проверка... 53 | Недоступно для прямого скачивания 54 | Установлен, не доступен онлайн 55 | Скачать 56 | Скачать заново 57 | Загрузка... 58 | Скачать обновление 59 | Удалить 60 | Отменить загрузку 61 | Отменить удаление 62 | Отменить обновление 63 | Обновить статус 64 | Будет удалён при следующем входе в игру! 65 | Будет установлен при следующем входе в игру! 66 | Будет обновлён при следующем входе в игру! 67 | Установлен 68 | Устарел 69 | Подтверждённый создатель режима 70 | Не удалось загрузить rurusetto-addon: Пожалуйста, сообщите об этом в репозиторий rurusetto-addon, НЕ в репозиторий osu!lazer: Code {1} 71 | Главная 72 | Список изменений 73 | Версия неизвестна 74 | Rurusetto Addon 75 | Адрес API 76 | Безымянный режим 77 | О нет! 78 | Пожалуйста, убедитесь, что у вас есть подключение к интернету и адрес API в настройках не содержит ошибок 79 | Обновить 80 | Не удалось загрузить список режимов 81 | Не удалось загрузить страницу 82 | Не удалось загрузить подстраницы 83 | Что-то пошло не так, и я не знаю, что именно! 84 | Кажется, rurusetto addon не смог завершить предыдущую рабочую сессию. Пожалуйста, убедитесь, что ваши действия не могли вызвать ошибок 85 | Рекомендуемые карты 86 | 87 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/Resources/Textures/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flutterish/rurusetto-addon/47c7d97a249e8d4423360988dfc33655d775060e/osu.Game.Rulesets.RurusettoAddon/Resources/Textures/cover.jpg -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/Resources/Textures/default_pfp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flutterish/rurusetto-addon/47c7d97a249e8d4423360988dfc33655d775060e/osu.Game.Rulesets.RurusettoAddon/Resources/Textures/default_pfp.png -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/Resources/Textures/default_wiki_cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flutterish/rurusetto-addon/47c7d97a249e8d4423360988dfc33655d775060e/osu.Game.Rulesets.RurusettoAddon/Resources/Textures/default_wiki_cover.jpg -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/Resources/Textures/oh_no.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flutterish/rurusetto-addon/47c7d97a249e8d4423360988dfc33655d775060e/osu.Game.Rulesets.RurusettoAddon/Resources/Textures/oh_no.png -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/Resources/Textures/rurusetto-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flutterish/rurusetto-addon/47c7d97a249e8d4423360988dfc33655d775060e/osu.Game.Rulesets.RurusettoAddon/Resources/Textures/rurusetto-logo.png -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/RulesetDownloader.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Platform; 2 | using System.IO; 3 | using System.Net.Http; 4 | 5 | namespace osu.Game.Rulesets.RurusettoAddon { 6 | public class RulesetDownloader { 7 | RurusettoAPI API; 8 | Storage storage; 9 | 10 | public RulesetDownloader ( RurusettoAPI API, Storage storage ) { 11 | this.API = API; 12 | this.storage = storage; 13 | } 14 | 15 | Dictionary> downloadStates = new(); 16 | Dictionary> availabilities = new(); 17 | public Bindable GetStateBindable ( APIRuleset ruleset ) 18 | => downloadStates.GetOrAdd( ruleset, () => new Bindable( DownloadState.NotDownloading ) ); 19 | 20 | Bindable getAvailabilityBindable ( APIRuleset ruleset, bool checkOnCreate = true ) { 21 | return availabilities.GetOrAdd( ruleset, () => new Bindable( Availability.Unknown ), _ => { 22 | if ( checkOnCreate ) CheckAvailability( ruleset ); 23 | } ); 24 | } 25 | public Bindable GetAvailabilityBindable ( APIRuleset ruleset ) { 26 | return getAvailabilityBindable( ruleset, true ); 27 | } 28 | 29 | public void BindWith ( APIRuleset ruleset, IBindable bindable ) { 30 | bindable.BindTo( GetStateBindable( ruleset ) ); 31 | } 32 | public void BindWith ( APIRuleset ruleset, IBindable bindable ) { 33 | bindable.BindTo( GetAvailabilityBindable( ruleset ) ); 34 | } 35 | 36 | public void CheckAvailability ( APIRuleset ruleset ) { 37 | var availability = getAvailabilityBindable( ruleset, false ); 38 | 39 | availability.Value = Availability.Unknown; 40 | 41 | if ( ruleset.Source == Source.Local || ruleset.IsPresentLocally ) 42 | availability.Value |= Availability.AvailableLocally; 43 | else 44 | availability.Value |= Availability.NotAvailableLocally; 45 | 46 | if ( ruleset.Source == Source.Web ) { 47 | ruleset.RequestDetail( detail => { 48 | if ( detail.CanDownload ) 49 | availability.Value |= Availability.AvailableOnline; 50 | else 51 | availability.Value |= Availability.NotAvailableOnline; 52 | }, failure: _ => { 53 | availability.Value |= Availability.NotAvailableOnline; 54 | } ); 55 | 56 | if ( ruleset.ListingEntry?.Status is Status s ) { 57 | if ( File.Exists( ruleset.LocalPath ) ) { 58 | var info = new FileInfo( ruleset.LocalPath ); 59 | info.Refresh(); 60 | 61 | if ( s.LatestUpdate.HasValue && info.LastWriteTimeUtc < s.LatestUpdate.Value ) 62 | availability.Value |= Availability.Outdated; 63 | else if ( s.FileSize != 0 && info.Length != s.FileSize ) 64 | availability.Value |= Availability.Outdated; 65 | } 66 | } 67 | } 68 | else { 69 | availability.Value |= Availability.NotAvailableOnline; 70 | } 71 | } 72 | 73 | bool wasTaskCancelled ( APIRuleset ruleset, RulesetManagerTask task ) { 74 | return !tasks.TryGetValue( ruleset, out var currentTask ) || !ReferenceEquals( task, currentTask ); 75 | } 76 | 77 | Dictionary tasks = new(); 78 | void createDownloadTask ( APIRuleset ruleset, TaskType type, DownloadState duringState, DownloadState finishedState ) { 79 | var task = new RulesetManagerTask( type, null ); 80 | tasks[ ruleset ] = task; 81 | 82 | GetStateBindable( ruleset ).Value = duringState; 83 | 84 | ruleset.RequestDetail( async detail => { 85 | if ( wasTaskCancelled( ruleset, task ) ) return; 86 | 87 | if ( !detail.CanDownload ) { 88 | tasks.Remove( ruleset ); 89 | return; 90 | } 91 | 92 | var filename = $"./rurusetto-addon-temp/{detail.GithubFilename}"; 93 | if ( !storage.Exists( filename ) ) { 94 | using var data = await new HttpClient().GetStreamAsync( detail.Download ); 95 | if ( wasTaskCancelled( ruleset, task ) ) return; 96 | 97 | using var file = storage.GetStream( filename, FileAccess.Write, FileMode.OpenOrCreate ); 98 | await data.CopyToAsync( file ); 99 | 100 | if ( wasTaskCancelled( ruleset, task ) ) return; 101 | } 102 | 103 | tasks[ ruleset ] = task with { Source = filename }; 104 | GetStateBindable( ruleset ).Value = finishedState; 105 | 106 | }, failure: e => { 107 | if ( wasTaskCancelled( ruleset, task ) ) return; 108 | tasks.Remove( ruleset ); 109 | API.LogFailure( $"Downloader could not retrieve detail for {ruleset}", e ); 110 | } ); 111 | } 112 | 113 | public void DownloadRuleset ( APIRuleset ruleset ) { 114 | if ( tasks.TryGetValue( ruleset, out var task ) && task.Type == TaskType.Install ) 115 | return; 116 | 117 | createDownloadTask( ruleset, TaskType.Install, DownloadState.Downloading, DownloadState.ToBeImported ); 118 | } 119 | public void CancelRulesetDownload ( APIRuleset ruleset ) { 120 | if ( tasks.TryGetValue( ruleset, out var task ) && task.Type is TaskType.Install or TaskType.Update ) { 121 | tasks.Remove( ruleset ); 122 | GetStateBindable( ruleset ).Value = DownloadState.NotDownloading; 123 | } 124 | } 125 | 126 | public void UpdateRuleset ( APIRuleset ruleset ) { 127 | if ( tasks.TryGetValue( ruleset, out var task ) && task.Type == TaskType.Update ) 128 | return; 129 | 130 | createDownloadTask( ruleset, TaskType.Update, DownloadState.Downloading, DownloadState.ToBeImported ); 131 | } 132 | 133 | public void RemoveRuleset ( APIRuleset ruleset ) { 134 | if ( tasks.TryGetValue( ruleset, out var task ) ) { 135 | if ( task.Type == TaskType.Remove ) return; 136 | 137 | if ( task.Type is TaskType.Install or TaskType.Update ) { 138 | tasks.Remove( ruleset ); 139 | GetStateBindable( ruleset ).Value = DownloadState.NotDownloading; 140 | return; 141 | } 142 | } 143 | 144 | if ( string.IsNullOrWhiteSpace( ruleset.LocalPath ) ) { 145 | // TODO report this 146 | return; 147 | } 148 | 149 | tasks[ ruleset ] = new RulesetManagerTask( TaskType.Remove, ruleset.LocalPath ); 150 | GetStateBindable( ruleset ).Value = DownloadState.ToBeRemoved; 151 | } 152 | public void CancelRulesetRemoval ( APIRuleset ruleset ) { 153 | if ( tasks.TryGetValue( ruleset, out var task ) && task.Type == TaskType.Remove ) { 154 | tasks.Remove( ruleset ); 155 | GetStateBindable( ruleset ).Value = DownloadState.NotDownloading; 156 | } 157 | } 158 | 159 | /// 160 | /// Cleans up all files used by previous instances 161 | /// 162 | /// Whether the previous instance possibly finished its work. A value of means it definitely did not finish the work. 163 | public bool PerformPreCleanup () { 164 | foreach ( var i in storage.GetFiles( "./rulesets", "*.dll-removed" ) ) { 165 | storage.Delete( i ); 166 | } 167 | // we use this format, but the above was used previously so we should keep it 168 | foreach ( var i in storage.GetFiles( "./rulesets", "*.dll~" ) ) { 169 | storage.Delete( i ); 170 | } 171 | 172 | if ( storage.ExistsDirectory( "./rurusetto-addon-temp/" ) ) { 173 | storage.DeleteDirectory( "./rurusetto-addon-temp/" ); 174 | return false; 175 | } 176 | 177 | return true; 178 | } 179 | 180 | public void PerformTasks () { // TODO we can do this at runtime, with user consent as its probably not the greatest idea 181 | foreach ( var i in tasks.Values ) { 182 | if ( i.Source is null ) 183 | continue; 184 | 185 | if ( i.Type == TaskType.Install || i.Type == TaskType.Update ) { 186 | var filename = Path.GetFileName( i.Source ); 187 | var path = storage.GetFullPath( $"./rulesets/{filename}" ); 188 | if ( File.Exists( path ) ) { 189 | File.Move( path, path + "~" ); 190 | } 191 | 192 | var to = storage.GetStream( $"./rulesets/{filename}", FileAccess.Write, FileMode.CreateNew ); 193 | var from = storage.GetStream( i.Source, FileAccess.Read, FileMode.Open ); 194 | 195 | from.CopyTo( to ); 196 | 197 | from.Dispose(); 198 | to.Dispose(); 199 | } 200 | else if ( i.Type == TaskType.Remove ) { 201 | File.Move( i.Source, i.Source + "~" ); 202 | } 203 | } 204 | 205 | if ( storage.ExistsDirectory( "./rurusetto-addon-temp/" ) ) { 206 | storage.DeleteDirectory( "./rurusetto-addon-temp/" ); 207 | } 208 | 209 | tasks.Clear(); 210 | } 211 | } 212 | 213 | public enum DownloadState { 214 | NotDownloading, 215 | 216 | Downloading, 217 | ToBeImported, 218 | ToBeRemoved 219 | } 220 | 221 | [Flags] 222 | public enum Availability { 223 | Unknown = 0, 224 | 225 | NotAvailableLocally = 1, 226 | AvailableLocally = 2, 227 | NotAvailableOnline = 4, 228 | AvailableOnline = 8, 229 | Outdated = 16 230 | } 231 | 232 | public record RulesetManagerTask ( TaskType Type, string? Source ); 233 | public enum TaskType { 234 | Install, 235 | Update, 236 | Remove 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/RurusettoAddonRuleset.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Textures; 2 | using osu.Framework.Input; 3 | using osu.Framework.Input.Bindings; 4 | using osu.Framework.Platform; 5 | using osu.Game.Beatmaps; 6 | using osu.Game.Configuration; 7 | using osu.Game.Overlays.Settings; 8 | using osu.Game.Rulesets.Configuration; 9 | using osu.Game.Rulesets.Difficulty; 10 | using osu.Game.Rulesets.Difficulty.Preprocessing; 11 | using osu.Game.Rulesets.Difficulty.Skills; 12 | using osu.Game.Rulesets.Mods; 13 | using osu.Game.Rulesets.Objects; 14 | using osu.Game.Rulesets.Objects.Drawables; 15 | using osu.Game.Rulesets.RurusettoAddon.Configuration; 16 | using osu.Game.Rulesets.RurusettoAddon.UI; 17 | using osu.Game.Rulesets.UI; 18 | using System.Threading; 19 | 20 | namespace osu.Game.Rulesets.RurusettoAddon; 21 | 22 | public class RurusettoAddonRuleset : Ruleset { 23 | public override string Description => "rūrusetto addon"; 24 | public const string SHORT_NAME = "rurusettoaddon"; 25 | public override string ShortName => SHORT_NAME; 26 | 27 | public override IRulesetConfigManager CreateConfig ( SettingsStore settings ) 28 | => new RurusettoConfigManager( settings, RulesetInfo ); 29 | public override RulesetSettingsSubsection CreateSettings () 30 | => new RurusettoAddonConfigSubsection( this ); 31 | 32 | public override DrawableRuleset CreateDrawableRulesetWith ( IBeatmap beatmap, IReadOnlyList? mods = null ) 33 | => new DrawableRurusettoAddonRuleset( this, beatmap, mods ); 34 | 35 | public override IBeatmapConverter CreateBeatmapConverter ( IBeatmap beatmap ) 36 | => new RurusettoAddonBeatmapConverter( beatmap, this ); 37 | 38 | public override DifficultyCalculator CreateDifficultyCalculator ( IWorkingBeatmap beatmap ) 39 | => new RurusettoAddonDifficultyCalculator( RulesetInfo, beatmap ); 40 | 41 | public override IEnumerable GetModsFor ( ModType type ) 42 | => Array.Empty(); 43 | 44 | public override IEnumerable GetDefaultKeyBindings ( int variant = 0 ) 45 | => Array.Empty(); 46 | 47 | public Texture GetTexture ( GameHost host, TextureStore textures, string path ) { 48 | if ( !textures.GetAvailableResources().Contains( path ) ) 49 | textures.AddTextureSource( host.CreateTextureLoaderStore( CreateResourceStore() ) ); 50 | 51 | return textures.Get( path ); 52 | } 53 | 54 | public override Drawable CreateIcon () => new RurusettoIcon( this ); 55 | } 56 | 57 | #region Vestigial organs 58 | 59 | public class RurusettoAddonPlayfield : Playfield { } 60 | public enum RurusettoAddonAction { } 61 | public class RurusettoAddonInputManager : RulesetInputManager { 62 | public RurusettoAddonInputManager ( RulesetInfo ruleset ) : base( ruleset, 0, SimultaneousBindingMode.Unique ) { } 63 | } 64 | public class RurusettoAddonDifficultyCalculator : DifficultyCalculator { 65 | public RurusettoAddonDifficultyCalculator ( IRulesetInfo ruleset, IWorkingBeatmap beatmap ) : base( ruleset, beatmap ) { } 66 | 67 | protected override DifficultyAttributes CreateDifficultyAttributes ( IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate ) 68 | => new( mods, 0 ); 69 | 70 | protected override IEnumerable CreateDifficultyHitObjects ( IBeatmap beatmap, double clockRate ) 71 | => Array.Empty(); 72 | 73 | protected override Skill[] CreateSkills ( IBeatmap beatmap, Mod[] mods, double clockRate ) 74 | => Array.Empty(); 75 | } 76 | public class RurusettoAddonBeatmapConverter : BeatmapConverter { 77 | public RurusettoAddonBeatmapConverter ( IBeatmap beatmap, Ruleset ruleset ) : base( beatmap, ruleset ) { } 78 | 79 | public override bool CanConvert () => false; 80 | 81 | protected override IEnumerable ConvertHitObject ( HitObject original, IBeatmap beatmap, CancellationToken cancellationToken ) { 82 | yield break; 83 | } 84 | } 85 | public class DrawableRurusettoAddonRuleset : DrawableRuleset { 86 | public DrawableRurusettoAddonRuleset ( RurusettoAddonRuleset ruleset, IBeatmap beatmap, IReadOnlyList? mods = null ) : base( ruleset, beatmap, mods ) { } 87 | 88 | protected override Playfield CreatePlayfield () 89 | => new RurusettoAddonPlayfield(); 90 | 91 | public override DrawableHitObject? CreateDrawableRepresentation ( HitObject h ) 92 | => null; 93 | 94 | protected override PassThroughInputManager CreateInputManager () 95 | => new RurusettoAddonInputManager( Ruleset.RulesetInfo ); 96 | } 97 | 98 | #endregion -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/RurusettoIcon.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Textures; 2 | using osu.Framework.Localisation; 3 | using osu.Framework.Platform; 4 | using osu.Game.Overlays; 5 | using osu.Game.Overlays.Notifications; 6 | using osu.Game.Overlays.Toolbar; 7 | using System.Diagnostics.CodeAnalysis; 8 | 9 | namespace osu.Game.Rulesets.RurusettoAddon; 10 | 11 | public class RurusettoIcon : Sprite { 12 | RurusettoAddonRuleset ruleset; 13 | 14 | public RurusettoIcon ( RurusettoAddonRuleset ruleset ) { 15 | this.ruleset = ruleset; 16 | 17 | RelativeSizeAxes = Axes.Both; 18 | FillMode = FillMode.Fit; 19 | Origin = Anchor.Centre; 20 | Anchor = Anchor.Centre; 21 | } 22 | 23 | [BackgroundDependencyLoader( permitNulls: true )] 24 | void load ( OsuGame game, GameHost host, TextureStore textures ) { 25 | Texture = ruleset.GetTexture( host, textures, "Textures/rurusetto-logo.png" ); 26 | 27 | injectOverlay( game, host ); 28 | } 29 | 30 | public static LocalisableString ErrorMessage ( string code ) 31 | => Localisation.Strings.LoadError( code ); 32 | 33 | // we are using the icon load code to inject our "mixin" since it is present in both the intro and the toolbar, where the overlay button should be 34 | void injectOverlay ( OsuGame game, GameHost host ) { 35 | if ( game is null ) return; 36 | if ( game.Dependencies.Get() != null ) return; 37 | 38 | var osu = (typeof( OsuGame ), game); 39 | 40 | var notifications = osu.GetField( "Notifications" ); 41 | if ( notifications is null ) 42 | return; 43 | 44 | void error ( string code ) { 45 | Schedule( () => notifications.Post( new SimpleErrorNotification { Text = ErrorMessage( code ) } ) ); 46 | } 47 | bool guard ( [NotNullWhen(false)] object? x, string code ) { 48 | if ( x is null ) { 49 | error( code ); 50 | return true; 51 | } 52 | return false; 53 | } 54 | 55 | // https://github.com/ppy/osu/blob/edf5e558aca6cd75e70b510a5f0dd233d6cfcb90/osu.Game/OsuGame.cs#L790 56 | // contains overlays 57 | var overlayContent = osu.GetField( "overlayContent" ); 58 | if ( guard( overlayContent, "#OCNRE" ) ) 59 | return; 60 | 61 | // https://github.com/ppy/osu/blob/edf5e558aca6cd75e70b510a5f0dd233d6cfcb90/osu.Game/OsuGame.cs#L953 62 | // caches the overlay globally and allows us to run code when it is loaded 63 | var loadComponent = osu.GetMethod( "loadComponentSingleFile" ); 64 | if ( guard( loadComponent, "#LCNRE" ) ) 65 | return; 66 | 67 | try { 68 | loadComponent.Invoke( game, 69 | new object[] { new RurusettoOverlay( ruleset ), (Action)addOverlay, true } 70 | ); 71 | } 72 | catch ( Exception ) { 73 | error( "#LCIE" ); 74 | } 75 | 76 | void addOverlay ( Drawable drawable ) { 77 | RurusettoOverlay overlay = (RurusettoOverlay)drawable; 78 | Action abort = () => { }; 79 | void errDefer ( Action action ) { 80 | var oldAbort = abort; 81 | abort = () => { action(); oldAbort(); }; 82 | } 83 | 84 | overlayContent.Add( overlay ); 85 | errDefer( () => overlayContent.Remove( overlay, false ) ); 86 | 87 | // https://github.com/ppy/osu/blob/edf5e558aca6cd75e70b510a5f0dd233d6cfcb90/osu.Game/Overlays/Toolbar/Toolbar.cs#L89 88 | // leveraging an "easy" hack to get the container with toolbar buttons 89 | var userButton = (typeof( Toolbar ), game.Toolbar).GetField( "userButton" ); 90 | if ( userButton is null || userButton.Parent is not FillFlowContainer buttonsContainer ) { 91 | error( "#UBNRE" ); 92 | abort(); 93 | return; 94 | } 95 | 96 | var button = new RurusettoToolbarButton(); 97 | buttonsContainer.Insert( -1, button ); 98 | errDefer( () => buttonsContainer.Remove( button, false ) ); 99 | 100 | // https://github.com/ppy/osu/blob/edf5e558aca6cd75e70b510a5f0dd233d6cfcb90/osu.Game/OsuGame.cs#L855 101 | // add overlay hiding, since osu does it manually 102 | var singleDisplayOverlays = new[] { "chatOverlay", "news", "dashboard", "beatmapListing", "changelogOverlay", "wikiOverlay" }; 103 | var overlays = singleDisplayOverlays.Select( name => osu.GetField( name ) ).ToList(); 104 | 105 | if ( !game.Dependencies.TryGet( out var rov ) ) { 106 | error( "#ROVNRE" ); 107 | abort(); 108 | return; 109 | } 110 | 111 | overlays.Add( rov ); 112 | 113 | if ( overlays.Any( x => x is null ) ) { 114 | error( "#OVNRE" ); 115 | abort(); 116 | return; 117 | } 118 | 119 | foreach ( var i in overlays ) { 120 | i!.State.ValueChanged += v => { 121 | if ( v.NewValue != Visibility.Visible ) return; 122 | 123 | overlay.Hide(); 124 | }; 125 | } 126 | 127 | overlay.State.ValueChanged += v => { 128 | if ( v.NewValue != Visibility.Visible ) return; 129 | 130 | foreach ( var i in overlays ) { 131 | i!.Hide(); 132 | } 133 | 134 | // https://github.com/ppy/osu/blob/edf5e558aca6cd75e70b510a5f0dd233d6cfcb90/osu.Game/OsuGame.cs#L896 135 | // show above other overlays 136 | if ( overlay.IsLoaded ) 137 | overlayContent.ChangeChildDepth( overlay, (float)-Clock.CurrentTime ); 138 | else 139 | overlay.Depth = (float)-Clock.CurrentTime; 140 | }; 141 | 142 | host.Exited += () => { 143 | overlay.Dependencies?.Get().PerformTasks(); 144 | }; 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/TextureNames.cs: -------------------------------------------------------------------------------- 1 | namespace osu.Game.Rulesets.RurusettoAddon; 2 | 3 | public static class TextureNames { 4 | public const string HeaderBackground = "Textures/cover.jpg"; 5 | public const string DefaultAvatar = "Textures/default_pfp.png"; 6 | public const string DefaultCover = "Textures/default_wiki_cover.jpg"; 7 | public const string ErrorCover = "Textures/oh_no.png"; 8 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/DrawableTag.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Cursor; 2 | using osu.Framework.Localisation; 3 | using osu.Game.Graphics.Sprites; 4 | 5 | namespace osu.Game.Rulesets.RurusettoAddon.UI; 6 | 7 | public class DrawableTag : CompositeDrawable, IHasTooltip { 8 | public DrawableTag ( LocalisableString tag, Colour4 colour, bool solid, float height = 18 ) { 9 | AutoSizeAxes = Axes.X; 10 | Height = height; 11 | 12 | if ( solid ) { 13 | AddInternal( new Box { 14 | RelativeSizeAxes = Axes.Both, 15 | Colour = colour 16 | } ); 17 | 18 | AddInternal( new Container { 19 | Padding = new MarginPadding { Horizontal = 4 }, 20 | AutoSizeAxes = Axes.Both, 21 | Child = new OsuSpriteText { 22 | Colour = Colour4.FromHex( "#191C17" ), 23 | UseFullGlyphHeight = false, 24 | Font = OsuFont.GetFont( Typeface.Torus, size: height - 2, weight: FontWeight.Bold ), 25 | Text = tag, 26 | Anchor = Anchor.CentreLeft, 27 | Origin = Anchor.CentreLeft 28 | }, 29 | Anchor = Anchor.CentreLeft, 30 | Origin = Anchor.CentreLeft 31 | } ); 32 | 33 | Masking = true; 34 | CornerRadius = 4; 35 | } 36 | else { 37 | Masking = true; 38 | CornerRadius = 4; 39 | BorderColour = colour; 40 | BorderThickness = 3; 41 | 42 | AddInternal( new Box { 43 | RelativeSizeAxes = Axes.Both, 44 | Colour = Colour4.Transparent 45 | } ); 46 | 47 | AddInternal( new Container { 48 | Padding = new MarginPadding { Horizontal = 4 }, 49 | AutoSizeAxes = Axes.Both, 50 | Child = new OsuSpriteText { 51 | Colour = colour, 52 | UseFullGlyphHeight = false, 53 | Font = OsuFont.GetFont( Typeface.Torus, size: height - 2, weight: FontWeight.Bold ), 54 | Text = tag, 55 | Anchor = Anchor.CentreLeft, 56 | Origin = Anchor.CentreLeft 57 | }, 58 | Anchor = Anchor.CentreLeft, 59 | Origin = Anchor.CentreLeft 60 | } ); 61 | } 62 | } 63 | 64 | public static DrawableTag CreateArchived ( bool large = false ) => new( Localisation.Strings.TagArchived, Colour4.FromHex( "#FFE766" ), solid: false, height: large ? 26 : 18 ) { 65 | TooltipText = Localisation.Strings.TagArchivedTooltip 66 | }; 67 | public static DrawableTag CreateLocal ( bool large = false ) => new( Localisation.Strings.TagLocal, Colour4.FromHex( "#FFE766" ), solid: true, height: large ? 26 : 18 ) { 68 | TooltipText = Localisation.Strings.TagLocalTooltip 69 | }; 70 | public static DrawableTag CreateHardCoded ( bool large = false ) => new( Localisation.Strings.TagHardcoded, Colour4.FromHex( "#FF6060" ), solid: true, height: large ? 26 : 18 ) { 71 | TooltipText = Localisation.Strings.TagHardcodedTooltip 72 | }; 73 | public static DrawableTag CreateFailledImport ( bool large = false ) => new( Localisation.Strings.TagFailedImport, Colour4.FromHex( "#FF6060" ), solid: true, height: large ? 26 : 18 ) { 74 | TooltipText = Localisation.Strings.TagFailedImportTooltip 75 | }; 76 | 77 | public static DrawableTag CreateBorked ( bool large = false ) => new( Localisation.Strings.TagBorked, Colour4.FromHex( "#FF6060" ), solid: false, height: large ? 26 : 18 ) { 78 | TooltipText = Localisation.Strings.TagBorkedTooltip 79 | }; 80 | public static DrawableTag CreatePlayable ( bool large = false ) => new( Localisation.Strings.TagPlayable, Colour4.FromHex( "#6CB946" ), solid: false, height: large ? 26 : 18 ) { 81 | TooltipText = Localisation.Strings.TagPlayableTooltip 82 | }; 83 | public static DrawableTag CreatePrerelease ( bool large = false ) => new( Localisation.Strings.TagPrerelease, Colour4.FromHex( "#FFE766" ), solid: false, height: large ? 26 : 18 ) { 84 | TooltipText = Localisation.Strings.TagPrereleaseTooltip 85 | }; 86 | 87 | 88 | public LocalisableString TooltipText { get; set; } 89 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/Listing/DrawableListingEntry.cs: -------------------------------------------------------------------------------- 1 | using Humanizer; 2 | using osu.Framework.Graphics.Textures; 3 | using osu.Framework.Input.Events; 4 | using osu.Framework.Localisation; 5 | using osu.Framework.Platform; 6 | using osu.Game.Graphics.Sprites; 7 | using osu.Game.Graphics.UserInterface; 8 | using osu.Game.Overlays; 9 | using osu.Game.Rulesets.RurusettoAddon.UI.Users; 10 | 11 | namespace osu.Game.Rulesets.RurusettoAddon.UI.Listing; 12 | 13 | public class DrawableListingEntry : VisibilityContainer { 14 | protected override bool StartHidden => false; 15 | 16 | private BufferedContainer content; 17 | protected override Container Content => content; 18 | 19 | [Resolved] 20 | protected RurusettoOverlay Overlay { get; private set; } = null!; 21 | [Resolved] 22 | protected RurusettoAPI API { get; private set; } = null!; 23 | [Resolved] 24 | protected RulesetDownloader Downloader { get; private set; } = null!; 25 | [Resolved] 26 | protected APIUserStore Users { get; private set; } = null!; 27 | public APIRuleset Ruleset { get; private set; } 28 | protected FillFlowContainer Tags; 29 | 30 | public DrawableListingEntry ( APIRuleset ruleset ) { 31 | Ruleset = ruleset; 32 | 33 | Height = 160; 34 | Masking = true; 35 | CornerRadius = 8; 36 | AlwaysPresent = true; 37 | 38 | Margin = new MarginPadding { 39 | Horizontal = 8, 40 | Vertical = 8 41 | }; 42 | 43 | Tags = new FillFlowContainer { 44 | Direction = FillDirection.Horizontal, 45 | AutoSizeAxes = Axes.Both, 46 | Spacing = new Vector2( 6, 0 ) 47 | }; 48 | 49 | AddInternal( content = new() { 50 | RelativeSizeAxes = Axes.Both 51 | } ); 52 | 53 | AddInternal( new RulesetManagementContextMenu( ruleset ) ); 54 | } 55 | 56 | ILocalisedBindableString? nameBindable; 57 | 58 | [BackgroundDependencyLoader] 59 | private void load ( OverlayColourProvider colours, LocalisationManager localisation, GameHost host, TextureStore textures, RurusettoAddonRuleset ruleset ) { 60 | var color = colours.Background4; 61 | Sprite cover; 62 | Drawable coverContainer; 63 | 64 | Add( new Box { 65 | Colour = color, 66 | RelativeSizeAxes = Axes.Both 67 | } ); 68 | Add( coverContainer = new Container { 69 | RelativeSizeAxes = Axes.X, 70 | Height = 80, 71 | Children = new Drawable[] { 72 | cover = new Sprite { 73 | RelativeSizeAxes = Axes.Both, 74 | FillMode = FillMode.Fill, 75 | Origin = Anchor.Centre, 76 | Anchor = Anchor.Centre, 77 | Scale = new Vector2( 1.2f ) 78 | }, 79 | new Box { 80 | Colour = new ColourInfo { 81 | HasSingleColour = false, 82 | BottomLeft = color.Opacity( 0.75f ), 83 | BottomRight = color.Opacity( 0.75f ), 84 | TopLeft = color.Opacity( 0.5f ), 85 | TopRight = color.Opacity( 0.5f ) 86 | }, 87 | RelativeSizeAxes = Axes.Both, 88 | Anchor = Anchor.BottomCentre, 89 | Origin = Anchor.BottomCentre 90 | } 91 | } 92 | } ); 93 | Add( new Box { 94 | Colour = color, 95 | RelativeSizeAxes = Axes.X, 96 | Height = Height - coverContainer.Height, 97 | Origin = Anchor.BottomLeft, 98 | Anchor = Anchor.BottomLeft 99 | } ); 100 | OsuSpriteText rulesetName; 101 | Add( new Container { 102 | Padding = new MarginPadding( 24f * 14 / 20 ) { Bottom = 24f * 14 / 20 - 4 }, 103 | RelativeSizeAxes = Axes.Both, 104 | Children = new Drawable[] { 105 | Tags, 106 | new RulesetLogo( Ruleset ) { 107 | Width = 80f * 14 / 20, 108 | Height = 80f * 14 / 20, 109 | Anchor = Anchor.CentreLeft, 110 | Origin = Anchor.CentreLeft 111 | }, 112 | new Container { 113 | AutoSizeAxes = Axes.X, 114 | Anchor = Anchor.CentreLeft, 115 | Origin = Anchor.CentreLeft, 116 | Height = 80f * 14 / 20, 117 | X = (80 + 12) * 14 / 20, 118 | Children = new Drawable[] { 119 | rulesetName = new OsuSpriteText { 120 | Font = OsuFont.GetFont( size: 24 ) 121 | }, 122 | new DrawableRurusettoUser( Users.GetUser( Ruleset.Owner ), Ruleset.IsVerified ) { 123 | Height = 34f * 14 / 20, 124 | Origin = Anchor.BottomLeft, 125 | Anchor = Anchor.BottomLeft 126 | } 127 | } 128 | }, 129 | new GridContainer { 130 | RelativeSizeAxes = Axes.X, 131 | Height = 50f * 14 / 20, 132 | Anchor = Anchor.BottomLeft, 133 | Origin = Anchor.BottomLeft, 134 | ColumnDimensions = new Dimension[] { 135 | new( GridSizeMode.Distributed ), 136 | new( GridSizeMode.Absolute, 50f * 14 / 20 ) 137 | }, 138 | RowDimensions = new Dimension[] { 139 | new( GridSizeMode.Distributed ) 140 | }, 141 | Content = new Drawable[][] { 142 | new Drawable[] { 143 | new TogglableScrollContainer { 144 | RelativeSizeAxes = Axes.X, 145 | Padding = new MarginPadding { Right = 4 }, 146 | Height = 30, 147 | ScrollbarVisible = false, 148 | Anchor = Anchor.BottomLeft, 149 | Origin = Anchor.BottomLeft, 150 | Child = new OsuTextFlowContainer( s => s.Font = OsuFont.GetFont( size: 14 ) ) { 151 | AutoSizeAxes = Axes.Y, 152 | RelativeSizeAxes = Axes.X, 153 | Text = Ruleset.Description 154 | } 155 | }, 156 | new RulesetDownloadButton( Ruleset ) { 157 | RelativeSizeAxes = Axes.Both, 158 | ProvideContextMenu = false 159 | } 160 | } 161 | } 162 | } 163 | } 164 | } ); 165 | 166 | nameBindable = localisation.GetLocalisedBindableString( Ruleset.Name ); 167 | nameBindable.BindValueChanged( v => { 168 | rulesetName.Text = v.NewValue.Humanize().ToLower(); 169 | }, true ); 170 | 171 | bool isCoverLoaded = false; 172 | cover.Texture = ruleset.GetTexture( host, textures, TextureNames.DefaultCover ); 173 | API.RequestImage( StaticAPIResource.DefaultCover, texture => { 174 | if ( !isCoverLoaded ) { 175 | cover.Texture = texture; 176 | } 177 | } ); 178 | 179 | Ruleset.RequestDarkCover( texture => { 180 | cover.Texture = texture; 181 | isCoverLoaded = true; 182 | } ); 183 | 184 | Ruleset.RequestDetail( detail => { 185 | Tags.AddRange( Ruleset.GenerateTags( detail ) ); 186 | } ); 187 | 188 | Add( new HoverClickSounds() ); 189 | } 190 | 191 | private bool isMaskedAway = true; 192 | protected override bool ComputeIsMaskedAway ( RectangleF maskingBounds ) { 193 | return isMaskedAway = base.ComputeIsMaskedAway( maskingBounds ); 194 | } 195 | 196 | protected override void Update () { 197 | base.Update(); 198 | 199 | var availableSize = Parent.ChildSize.X * 0.9f + Margin.Left + Margin.Right; 200 | const float minWidth = 280; 201 | int entriesPerLine = (int)Math.Max( 1, availableSize / ( minWidth + Margin.Left + Margin.Right ) ); 202 | Width = availableSize / entriesPerLine - Margin.Left - Margin.Right; 203 | 204 | if ( State.Value == Visibility.Hidden && !isMaskedAway ) { 205 | Show(); 206 | } 207 | } 208 | 209 | protected override void PopIn () { 210 | Alpha = 1; 211 | using ( BeginDelayedSequence( 100, true ) ) { 212 | content.MoveToY( 200 ).Then().MoveToY( 0, 500, Easing.Out ); 213 | content.FadeIn( 350 ); 214 | } 215 | } 216 | 217 | protected override void PopOut () { 218 | content.Alpha = 0; 219 | Alpha = 0; 220 | } 221 | 222 | protected override bool OnClick ( ClickEvent e ) { 223 | Overlay.Header.NavigateTo( 224 | Ruleset, 225 | Ruleset.Name == Localisation.Strings.UntitledRuleset 226 | ? Ruleset.Name 227 | : Ruleset.Name.ToString().Humanize().ToLower(), 228 | perserveCategories: true 229 | ); 230 | 231 | return true; 232 | } 233 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/Listing/ListingTab.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace osu.Game.Rulesets.RurusettoAddon.UI.Listing; 4 | 5 | public class ListingTab : OverlayTab { 6 | FillFlowContainer info; 7 | ListingEntryContainer content; 8 | Bindable apiAddress = new( RurusettoAPI.DefaultAPIAddress ); 9 | public ListingTab () { 10 | AddInternal( new FillFlowContainer() { 11 | Direction = FillDirection.Vertical, 12 | RelativeSizeAxes = Axes.X, 13 | AutoSizeAxes = Axes.Y, 14 | 15 | Children = new Drawable[] { 16 | info = new() { 17 | Direction = FillDirection.Full, 18 | RelativeSizeAxes = Axes.X, 19 | AutoSizeAxes = Axes.Y 20 | }, 21 | content = new() { 22 | Direction = FillDirection.Full, 23 | RelativeSizeAxes = Axes.X, 24 | AutoSizeAxes = Axes.Y, 25 | Padding = new MarginPadding { Horizontal = 32, Top = 8 } 26 | } 27 | } 28 | } ); 29 | } 30 | 31 | Task? refreshTask = null; 32 | public void ReloadListing () { 33 | Schedule( () => { 34 | Overlay.StartLoading( this ); 35 | content.Clear(); 36 | info.Clear(); 37 | } ); 38 | 39 | Task? task = null; 40 | task = refreshTask = Rulesets.RequestIdentities().ContinueWith( t => { 41 | Schedule( () => { 42 | Overlay.FinishLoadiong( this ); 43 | if ( task != refreshTask ) 44 | return; 45 | 46 | if ( !t.Result.ContainsWebListing ) { 47 | info.Add( new RequestFailedDrawable { 48 | ContentText = Localisation.Strings.ListingFetchError, 49 | ButtonClicked = () => Refresh() 50 | } ); 51 | } 52 | 53 | foreach ( var i in t.Result ) { 54 | content.Add( new DrawableListingEntry( i ) { 55 | Anchor = Anchor.TopCentre, 56 | Origin = Anchor.TopCentre 57 | } ); 58 | } 59 | } ); 60 | } ); 61 | } 62 | 63 | [BackgroundDependencyLoader] 64 | private void load () { 65 | apiAddress.BindTo( API.Address ); 66 | apiAddress.BindValueChanged( _ => Refresh() ); 67 | } 68 | 69 | public override bool Refresh () { 70 | API.FlushRulesetListingCache(); 71 | Rulesets.Refresh(); 72 | ReloadListing(); 73 | 74 | return true; 75 | } 76 | 77 | protected override void LoadContent () { 78 | ReloadListing(); 79 | } 80 | 81 | private class ListingEntryContainer : FillFlowContainer { 82 | Dictionary rulesets = new(); 83 | public override IEnumerable FlowingChildren => base.FlowingChildren.OrderBy( x => 84 | ( rulesets[x].Source == Source.Local ) ? 2 : 1 85 | ).ThenBy( x => 86 | ( rulesets[x].ListingEntry?.CanDownload == true ) ? 1 : 2 87 | ).ThenBy( x => 88 | ( rulesets[x].ListingEntry?.Status?.IsPlayable == true ) ? 1 : 2 89 | ).ThenBy( x => 90 | ( rulesets[x].ListingEntry?.Status?.IsBorked == true ) ? 3 : 2 91 | ).ThenByDescending( x => 92 | rulesets[x].ListingEntry?.Status?.LatestUpdate 93 | ); 94 | 95 | public override void Add ( DrawableListingEntry drawable ) { 96 | base.Add( drawable ); 97 | 98 | rulesets.Add( drawable, drawable.Ruleset ); 99 | } 100 | 101 | public override bool Remove ( DrawableListingEntry drawable, bool disposeImmediately ) { 102 | rulesets.Remove( drawable ); 103 | return base.Remove( drawable, disposeImmediately ); 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/Menus/LocalisableOsuMenuItem.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Localisation; 2 | using osu.Game.Graphics.UserInterface; 3 | 4 | namespace osu.Game.Rulesets.RurusettoAddon.UI.Menus; 5 | 6 | public class LocalisableOsuMenuItem : OsuMenuItem { 7 | public LocalisableOsuMenuItem ( LocalisableString text, MenuItemType type, Action action ) : base( "???", type, action ) { 8 | Text.Value = text; 9 | } 10 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/Overlay/CategorisedTabControlOverlayHeader.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Extensions; 2 | using osu.Framework.Graphics.UserInterface; 3 | using osu.Game.Graphics.UserInterface; 4 | using osu.Game.Overlays; 5 | 6 | namespace osu.Game.Rulesets.RurusettoAddon.UI.Overlay; 7 | 8 | public class CategorisedTabItem { 9 | public Tcategory Category { get; init; } = default!; 10 | public Ttab Tab { get; init; } = default!; 11 | } 12 | 13 | public abstract class CategorisedTabControlOverlayHeader : OverlayHeader, IHasCurrentValue 14 | where T : CategorisedTabItem 15 | where Tcategory : notnull { 16 | 17 | protected OsuTabControl TabControl; 18 | 19 | private readonly Box controlBackground; 20 | private readonly Container tabControlContainer; 21 | private readonly BindableWithCurrent current = new(); 22 | public Bindable Current { 23 | get => current.Current; 24 | set => current.Current = value; 25 | } 26 | 27 | protected OsuTabControl CategoryControl; 28 | 29 | private readonly Box categoryControlBackground; 30 | private readonly Container categoryControlContainer; 31 | private readonly BindableWithCurrent currentCategory = new(); 32 | public Bindable CurrentCategory { 33 | get => currentCategory.Current; 34 | set => currentCategory.Current = value; 35 | } 36 | 37 | protected new float ContentSidePadding { 38 | get => base.ContentSidePadding; 39 | set { 40 | base.ContentSidePadding = value; 41 | tabControlContainer.Padding = new MarginPadding { Horizontal = value }; 42 | } 43 | } 44 | 45 | protected CategorisedTabControlOverlayHeader () { 46 | new Container { 47 | RelativeSizeAxes = Axes.X, 48 | AutoSizeAxes = Axes.Y, 49 | Depth = -1, 50 | Children = new Drawable[] { 51 | categoryControlBackground = new Box 52 | { 53 | RelativeSizeAxes = Axes.Both, 54 | }, 55 | categoryControlContainer = new Container 56 | { 57 | RelativeSizeAxes = Axes.X, 58 | AutoSizeAxes = Axes.Y, 59 | Padding = new MarginPadding { Horizontal = ContentSidePadding }, 60 | Child = CategoryControl = CreateCategoryControl().With(control => 61 | { 62 | control.Current = CurrentCategory; 63 | }) 64 | } 65 | } 66 | }; 67 | HeaderInfo.Add( new FillFlowContainer { 68 | RelativeSizeAxes = Axes.X, 69 | AutoSizeAxes = Axes.Y, 70 | Direction = FillDirection.Vertical, 71 | Children = new Drawable[] 72 | { 73 | new Container { 74 | RelativeSizeAxes = Axes.X, 75 | AutoSizeAxes = Axes.Y, 76 | Children = new Drawable[] { 77 | controlBackground = new Box 78 | { 79 | RelativeSizeAxes = Axes.Both, 80 | }, 81 | tabControlContainer = new Container 82 | { 83 | RelativeSizeAxes = Axes.X, 84 | AutoSizeAxes = Axes.Y, 85 | Padding = new MarginPadding { Horizontal = ContentSidePadding }, 86 | Child = TabControl = CreateTabControl().With(control => 87 | { 88 | control.Current = Current; 89 | }) 90 | } 91 | } 92 | } 93 | } 94 | } ); 95 | } 96 | 97 | [BackgroundDependencyLoader] 98 | private void load ( OverlayColourProvider colourProvider ) { 99 | controlBackground.Colour = colourProvider.Dark3; 100 | categoryControlBackground.Colour = colourProvider.Dark4; 101 | } 102 | 103 | protected virtual OsuTabControl CreateTabControl () => new OverlayHeaderTabControl(); 104 | protected virtual OsuTabControl CreateCategoryControl () => new OverlayHeaderTabControl(); 105 | } 106 | 107 | public class OverlayHeaderTabControl : OverlayTabControl where T : notnull { 108 | private const float bar_height = 1; 109 | 110 | public OverlayHeaderTabControl () { 111 | RelativeSizeAxes = Axes.None; 112 | AutoSizeAxes = Axes.X; 113 | Anchor = Anchor.BottomLeft; 114 | Origin = Anchor.BottomLeft; 115 | Height = 47; 116 | BarHeight = bar_height; 117 | } 118 | 119 | protected override TabItem CreateTabItem ( T value ) => new OverlayHeaderTabItem( value ); 120 | 121 | protected override TabFillFlowContainer CreateTabFlow () => new TabFillFlowContainer { 122 | RelativeSizeAxes = Axes.Y, 123 | AutoSizeAxes = Axes.X, 124 | Direction = FillDirection.Horizontal, 125 | }; 126 | 127 | private class OverlayHeaderTabItem : OverlayTabItem { 128 | public OverlayHeaderTabItem ( T value ) : base( value ) { 129 | if ( Value is Enum enumValue ) { 130 | var localisableDescription = enumValue.GetLocalisableDescription(); 131 | string nonLocalisableDescription = enumValue.GetDescription(); 132 | 133 | // If localisable == non-localisable, then we must have a basic string, so .ToLower() is used. 134 | Text.Text = localisableDescription.Equals( nonLocalisableDescription ) 135 | ? nonLocalisableDescription.ToLower() 136 | : localisableDescription; 137 | } 138 | else { 139 | Text.Text = Value.ToString()!.ToLower(); 140 | } 141 | 142 | Text.Font = OsuFont.GetFont( size: 14 ); 143 | Text.Margin = new MarginPadding { Vertical = 16.5f }; // 15px padding + 1.5px line-height difference compensation 144 | Bar.Margin = new MarginPadding { Bottom = bar_height }; 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/Overlay/RurusettoOverlay.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Input.Events; 2 | using osu.Framework.Platform; 3 | using osu.Game.Graphics.Cursor; 4 | using osu.Game.Graphics.UserInterface; 5 | using osu.Game.Input.Bindings; 6 | using osu.Game.Overlays; 7 | using osu.Game.Overlays.Notifications; 8 | using osu.Game.Rulesets.RurusettoAddon.Configuration; 9 | using osu.Game.Rulesets.RurusettoAddon.UI.Listing; 10 | using osu.Game.Rulesets.RurusettoAddon.UI.Users; 11 | using osu.Game.Rulesets.RurusettoAddon.UI.Wiki; 12 | using osuTK.Input; 13 | 14 | namespace osu.Game.Rulesets.RurusettoAddon.UI.Overlay; 15 | 16 | [Cached] 17 | public class RurusettoOverlay : FullscreenOverlay { 18 | FillFlowContainer content; 19 | OverlayScrollContainer scroll; 20 | Container tabContainer; 21 | RurusettoAddonRuleset ruleset; 22 | 23 | LoadingLayer loading; 24 | OverlayTab currentTab; 25 | ListingTab listing; 26 | Dictionary infoTabs = new(); 27 | Dictionary userTabs = new(); 28 | 29 | [Cached] 30 | new RurusettoAPI API = new(); 31 | 32 | protected override IReadOnlyDependencyContainer CreateChildDependencies ( IReadOnlyDependencyContainer parent ) { 33 | var dep = new DependencyContainer( base.CreateChildDependencies( parent ) ); 34 | 35 | dep.CacheAs( ruleset ); 36 | dep.CacheAs( new APIRulesetStore { 37 | Storage = dep.Get(), 38 | RulesetStore = dep.Get(), 39 | API = API 40 | } ); 41 | dep.CacheAs( new APIUserStore( API ) ); 42 | RulesetDownloader download; 43 | dep.CacheAs( download = new( API, dep.Get() ) ); 44 | if ( !download.PerformPreCleanup() ) { 45 | Schedule( () => dep.Get()?.Post( new SimpleErrorNotification { Text = Localisation.Strings.NotificationWorkIncomplete } ) ); 46 | } 47 | 48 | dep.Get>()?.BindValueChanged( v => { 49 | if ( v.NewValue.ShortName == RurusettoAddonRuleset.SHORT_NAME ) { 50 | Show(); 51 | } 52 | } ); 53 | 54 | Schedule( () => { 55 | AddInternal( API ); 56 | } ); 57 | 58 | try { 59 | var rulesetconfig = dep.Get(); 60 | var config = rulesetconfig?.GetConfigFor( ruleset ) as RurusettoConfigManager; 61 | 62 | config?.BindWith( RurusettoSetting.APIAddress, API.Address ); 63 | } 64 | catch ( Exception ) { } 65 | 66 | return dep; 67 | } 68 | 69 | public RurusettoOverlay ( RurusettoAddonRuleset ruleset ) : base( OverlayColourScheme.Pink ) { 70 | this.ruleset = ruleset; 71 | 72 | Add( new OsuContextMenuContainer { 73 | RelativeSizeAxes = Axes.Both, 74 | Child = scroll = new OverlayScrollContainer { 75 | RelativeSizeAxes = Axes.Both, 76 | ScrollbarVisible = false, 77 | 78 | Child = content = new FillFlowContainer { 79 | Direction = FillDirection.Vertical, 80 | RelativeSizeAxes = Axes.X, 81 | AutoSizeAxes = Axes.Y 82 | } 83 | } 84 | } ); 85 | 86 | Header.Depth = -1; 87 | content.Add( Header ); 88 | content.Add( tabContainer = new() { 89 | RelativeSizeAxes = Axes.X, 90 | AutoSizeAxes = Axes.Y 91 | } ); 92 | tabContainer.OnUpdate += x => { 93 | x.Margin = x.Margin with { Bottom = DrawHeight * 3 / 5 }; 94 | }; 95 | 96 | tabContainer.Add( currentTab = listing = new() ); 97 | Schedule( () => { 98 | listing.Show(); 99 | listing.ReloadListing(); 100 | } ); 101 | 102 | Add( loading = new LoadingLayer( dimBackground: true ) ); 103 | 104 | Header.Current.ValueChanged += _ => onSelectedInfoChanged(); 105 | } 106 | 107 | private void onSelectedInfoChanged () { 108 | OverlayTab tab = listing; 109 | 110 | switch ( Header.Current.Value ) { 111 | case { Tab: APIRuleset ruleset }: 112 | if ( !infoTabs.TryGetValue( ruleset, out var rulesetTab ) ) { 113 | infoTabs.Add( ruleset, rulesetTab = new( ruleset ) ); 114 | tabContainer.Add( rulesetTab ); 115 | } 116 | tab = rulesetTab; 117 | break; 118 | 119 | case { Tab: APIUser user }: 120 | if ( !userTabs.TryGetValue( user, out var userTab ) ) { 121 | userTabs.Add( user, userTab = new( user ) ); 122 | tabContainer.Add( userTab ); 123 | } 124 | tab = userTab; 125 | break; 126 | }; 127 | 128 | presentTab( tab ); 129 | } 130 | 131 | private void presentTab ( OverlayTab tab ) { 132 | scroll.ScrollToStart(); 133 | currentTab.Hide(); 134 | 135 | currentTab = tab; 136 | 137 | currentTab.Show(); 138 | tabContainer.ChangeChildDepth( currentTab, (float)-Clock.CurrentTime ); 139 | updateLoading(); 140 | } 141 | 142 | bool isFullHidden; 143 | protected override void PopIn () { 144 | base.PopIn(); 145 | 146 | if ( isFullHidden ) { 147 | listing.Refresh(); 148 | isFullHidden = false; 149 | } 150 | 151 | scroll.ScrollToStart(); 152 | } 153 | 154 | protected override void PopOutComplete () { 155 | base.PopOutComplete(); 156 | 157 | loadingTabs.Clear(); 158 | updateLoading(); 159 | 160 | if ( Header.Current.Value.Tab is null && Header.CurrentCategory.Value == Header.ListingTab.Category ) { 161 | foreach ( var i in infoTabs ) { 162 | tabContainer.Remove( i.Value, false ); 163 | i.Value.Dispose(); 164 | } 165 | userTabs.Clear(); 166 | infoTabs.Clear(); 167 | } 168 | isFullHidden = true; 169 | } 170 | 171 | Dictionary loadingTabs = new(); 172 | public void StartLoading ( OverlayTab tab ) { 173 | if ( loadingTabs.ContainsKey( tab ) ) { 174 | loadingTabs[tab]++; 175 | } 176 | else { 177 | loadingTabs.Add( tab, 1 ); 178 | } 179 | updateLoading(); 180 | } 181 | 182 | public void FinishLoadiong ( OverlayTab tab ) { 183 | if ( loadingTabs.ContainsKey( tab ) ) { 184 | loadingTabs[tab]--; 185 | 186 | if ( loadingTabs[tab] <= 0 ) { 187 | loadingTabs.Remove( tab ); 188 | } 189 | } 190 | updateLoading(); 191 | } 192 | 193 | private void updateLoading () { 194 | if ( loadingTabs.ContainsKey( currentTab ) ) { 195 | loading.Show(); 196 | } 197 | else { 198 | loading.Hide(); 199 | } 200 | } 201 | 202 | protected override RurusettoOverlayHeader CreateHeader () 203 | => new(); 204 | 205 | public override bool OnPressed ( KeyBindingPressEvent e ) { 206 | if ( e.Action == GlobalAction.Back && Header.NavigateBack() ) { 207 | return true; 208 | } 209 | 210 | return base.OnPressed( e ); 211 | } 212 | 213 | protected override bool OnKeyDown ( KeyDownEvent e ) { 214 | if ( ( e.AltPressed || e.ControlPressed ) && e.Key == Key.Left ) { 215 | Header.NavigateBack(); 216 | return true; 217 | } 218 | if ( ( e.AltPressed || e.ControlPressed ) && e.Key == Key.Right ) { 219 | Header.NavigateForward(); 220 | return true; 221 | } 222 | return false; 223 | } 224 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/Overlay/RurusettoOverlayBackground.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Textures; 2 | using osu.Framework.Platform; 3 | 4 | namespace osu.Game.Rulesets.RurusettoAddon.UI.Overlay; 5 | 6 | public class RurusettoOverlayBackground : CompositeDrawable { 7 | public RurusettoOverlayBackground () { 8 | Height = 80; 9 | RelativeSizeAxes = Axes.X; 10 | 11 | Masking = true; 12 | } 13 | 14 | private Dictionary covers = new(); 15 | Sprite? currentCover; 16 | Texture defaultCover = null!; 17 | 18 | [BackgroundDependencyLoader] 19 | private void load ( GameHost host, TextureStore textures, RurusettoAddonRuleset ruleset ) { 20 | SetCover( defaultCover = ruleset.GetTexture( host, textures, TextureNames.HeaderBackground ), expanded: true ); 21 | } 22 | 23 | public void SetCover ( Texture? cover, bool expanded ) { 24 | cover ??= defaultCover; 25 | 26 | if ( !covers.TryGetValue( cover, out var sprite ) ) { 27 | AddInternal( sprite = new Sprite { 28 | RelativeSizeAxes = Axes.Both, 29 | Texture = cover, 30 | FillMode = FillMode.Fill, 31 | Anchor = Anchor.Centre, 32 | Origin = Anchor.Centre 33 | } ); 34 | 35 | covers.Add( cover, sprite ); 36 | } 37 | 38 | currentCover?.FadeOut( 400 ); 39 | ChangeInternalChildDepth( sprite, (float)Clock.CurrentTime ); 40 | sprite.FadeIn(); 41 | 42 | currentCover = sprite; 43 | 44 | if ( expanded ) { 45 | this.FadeIn().ResizeHeightTo( 80, 400, Easing.Out ); 46 | } 47 | else { 48 | this.ResizeHeightTo( 0, 400, Easing.Out ).Then().FadeOut(); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/Overlay/RurusettoOverlayHeader.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.UserInterface; 2 | using osu.Framework.Localisation; 3 | using osu.Game.Graphics.UserInterface; 4 | using osu.Game.Overlays; 5 | 6 | namespace osu.Game.Rulesets.RurusettoAddon.UI.Overlay; 7 | 8 | public class RurusettoTabItem : CategorisedTabItem { 9 | public LocalisableString Title { get; init; } 10 | } 11 | 12 | public class RurusettoOverlayHeader : CategorisedTabControlOverlayHeader { 13 | public readonly RurusettoTabItem ListingTab = new() { 14 | Category = Localisation.Strings.ListingTab, 15 | Title = Localisation.Strings.ListingTab 16 | }; 17 | public readonly RurusettoTabItem UsersTab = new() { 18 | Category = Localisation.Strings.UsersTab, 19 | Title = Localisation.Strings.UsersTab 20 | }; 21 | public readonly RurusettoTabItem CollectionsTab = new() { 22 | Category = Localisation.Strings.CollectionsTab, 23 | Title = Localisation.Strings.CollectionsTab 24 | }; 25 | 26 | [Resolved] 27 | protected RurusettoAPI API { get; private set; } = null!; 28 | 29 | private bool userNaviaged = true; 30 | private int navigationDirection = -1; 31 | public RurusettoOverlayHeader () { 32 | TabControl.AddItem( ListingTab ); 33 | TabControl.Current.Value = ListingTab; 34 | 35 | Current.ValueChanged += v => { 36 | CategoryControl.Current.Value = v.NewValue.Category; 37 | if ( userNaviaged && v.NewValue.Tab is null ) { 38 | if ( navigationDirection == 1 ) 39 | NavigateForward(); 40 | else 41 | NavigateBack(); 42 | } 43 | 44 | switch ( v.NewValue ) { 45 | case { Tab: APIRuleset ruleset }: 46 | ruleset.RequestDarkCover( texture => { 47 | background.SetCover( texture, expanded: false ); 48 | } ); 49 | break; 50 | 51 | case { Tab: APIUser user }: 52 | user.RequestDarkCover( cover => { 53 | background.SetCover( cover, expanded: true ); 54 | } ); 55 | break; 56 | 57 | default: 58 | background.SetCover( null, expanded: true ); 59 | break; 60 | } 61 | }; 62 | 63 | CategoryControl.AddItem( ListingTab.Category ); 64 | CategoryControl.AddItem( UsersTab.Category ); 65 | CategoryControl.AddItem( CollectionsTab.Category ); 66 | 67 | CategoryControl.Current.ValueChanged += v => { 68 | NavigateTo( categoryFromName( v.NewValue ) ); 69 | }; 70 | } 71 | 72 | private RurusettoTabItem categoryFromName ( LocalisableString name ) { 73 | return name == Localisation.Strings.ListingTab 74 | ? ListingTab 75 | : name == Localisation.Strings.UsersTab 76 | ? UsersTab 77 | : CollectionsTab; 78 | } 79 | 80 | public void NavigateTo ( RurusettoTabItem tab, bool perserveCategories = false ) { 81 | if ( categoryFromName( Current.Value.Category ) == tab ) 82 | return; 83 | 84 | RurusettoTabItem item = new() { 85 | Tab = tab.Tab, 86 | Title = tab.Title, 87 | Category = tab.Category 88 | }; 89 | 90 | if ( !perserveCategories && Current.Value.Category != tab.Category ) { 91 | TabControl.Clear(); 92 | } 93 | 94 | clearHistoryAfterCurrent(); 95 | TabControl.AddItem( item ); 96 | userNaviaged = false; 97 | TabControl.Current.Value = item; 98 | userNaviaged = true; 99 | } 100 | 101 | public void NavigateTo ( object tab, LocalisableString title, bool perserveCategories = false ) { 102 | if ( tab == Current.Value.Tab ) 103 | return; 104 | 105 | var category = tab switch { 106 | APIRuleset => ListingTab, 107 | APIUser => UsersTab, 108 | _ => CollectionsTab 109 | }; 110 | RurusettoTabItem item = new() { 111 | Tab = tab, 112 | Title = title, 113 | Category = category.Category 114 | }; 115 | 116 | if ( item.Category != Current.Value.Category ) { 117 | NavigateTo( categoryFromName( item.Category ), perserveCategories ); 118 | } 119 | 120 | clearHistoryAfterCurrent(); 121 | TabControl.AddItem( item ); 122 | userNaviaged = false; 123 | TabControl.Current.Value = item; 124 | userNaviaged = true; 125 | } 126 | 127 | private void clearHistoryAfterCurrent () { 128 | while ( TabControl.Items.Any() && TabControl.Items[^1] != Current.Value ) { 129 | TabControl.RemoveItem( TabControl.Items[^1] ); 130 | } 131 | } 132 | 133 | public bool NavigateBack () { 134 | if ( TabControl.Items[0] == Current.Value ) 135 | return false; 136 | 137 | var prevDir = navigationDirection; 138 | 139 | navigationDirection = -1; 140 | TabControl.SwitchTab( -1, wrap: false ); 141 | navigationDirection = prevDir; 142 | return true; 143 | } 144 | 145 | public bool NavigateForward () { 146 | if ( TabControl.Items[^1] == Current.Value ) 147 | return false; 148 | 149 | var prevDir = navigationDirection; 150 | 151 | navigationDirection = 1; 152 | TabControl.SwitchTab( 1, wrap: false ); 153 | navigationDirection = prevDir; 154 | return true; 155 | } 156 | 157 | protected override OverlayTitle CreateTitle () 158 | => new HeaderTitle(); 159 | 160 | private RurusettoOverlayBackground background = null!; 161 | protected override RurusettoOverlayBackground CreateBackground () 162 | => background = new RurusettoOverlayBackground(); 163 | 164 | private class HeaderTitle : OverlayTitle { 165 | public HeaderTitle () { 166 | Title = "rūrusetto"; 167 | Description = Localisation.Strings.RurusettoDescription; 168 | Icon = OsuIcon.Rulesets; 169 | } 170 | } 171 | 172 | protected override OsuTabControl CreateTabControl () => new OverlayHeaderBreadcrumbControl(); 173 | 174 | public class OverlayHeaderBreadcrumbControl : BreadcrumbControl { 175 | public OverlayHeaderBreadcrumbControl () { 176 | RelativeSizeAxes = Axes.X; 177 | Height = 47; 178 | 179 | Current.ValueChanged += index => { 180 | if ( index.NewValue is null ) 181 | return; 182 | 183 | var category = Items[0].Category; 184 | var prev = (ControlTabItem)TabMap[Items[0]]; 185 | foreach ( var item in TabContainer.Children.OfType().Skip( 1 ) ) { 186 | if ( item.Value.Category != category ) { 187 | prev.Chevron.Icon = FontAwesome.Solid.AngleDoubleRight; 188 | category = item.Value.Category; 189 | } 190 | else { 191 | prev.Chevron.Icon = FontAwesome.Solid.ChevronRight; 192 | } 193 | 194 | prev = item; 195 | } 196 | }; 197 | } 198 | 199 | protected override Dropdown CreateDropdown () { 200 | return new ControlDropdown(); 201 | } 202 | 203 | [BackgroundDependencyLoader] 204 | private void load ( OverlayColourProvider colourProvider ) { 205 | AccentColour = colourProvider.Light2; 206 | } 207 | 208 | protected override TabItem CreateTabItem ( RurusettoTabItem value ) => new ControlTabItem( value ) { 209 | AccentColour = AccentColour, 210 | }; 211 | 212 | private class ControlDropdown : OsuTabDropdown { 213 | protected override LocalisableString GenerateItemText ( RurusettoTabItem item ) 214 | => item.Title; 215 | } 216 | 217 | private class ControlTabItem : BreadcrumbTabItem { 218 | protected override float ChevronSize => 8; 219 | 220 | public ControlTabItem ( RurusettoTabItem value ) 221 | : base( value ) { 222 | RelativeSizeAxes = Axes.Y; 223 | Text.Font = Text.Font.With( size: 14 ); 224 | Text.Anchor = Anchor.CentreLeft; 225 | Text.Origin = Anchor.CentreLeft; 226 | Chevron.Y = 1; 227 | Bar.Height = 0; 228 | AlwaysPresent = true; 229 | } 230 | 231 | protected override void LoadComplete () { 232 | base.LoadComplete(); 233 | 234 | Text.Text = Value.Title; 235 | } 236 | 237 | protected override void Update () { 238 | base.Update(); 239 | if ( Alpha == 0 ) { 240 | AutoSizeAxes = Axes.None; 241 | Width = 0; 242 | } 243 | else { 244 | AutoSizeAxes = Axes.X; 245 | } 246 | } 247 | 248 | // base OsuTabItem makes font bold on activation, we don't want that here 249 | protected override void OnActivated () => FadeHovered(); 250 | 251 | protected override void OnDeactivated () => FadeUnhovered(); 252 | } 253 | } 254 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/Overlay/RurusettoToolbarButton.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Overlays.Toolbar; 2 | 3 | namespace osu.Game.Rulesets.RurusettoAddon.UI.Overlay; 4 | 5 | public class RurusettoToolbarButton : ToolbarOverlayToggleButton { 6 | protected override Anchor TooltipAnchor => Anchor.TopRight; 7 | 8 | public RurusettoToolbarButton () { 9 | //Hotkey = GlobalAction.ToggleChat; 10 | } 11 | 12 | [BackgroundDependencyLoader( true )] 13 | private void load ( RurusettoOverlay overlay ) { 14 | StateContainer = overlay; 15 | } 16 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/OverlayTab.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Input.Events; 2 | using osuTK.Input; 3 | 4 | namespace osu.Game.Rulesets.RurusettoAddon.UI; 5 | 6 | public abstract class OverlayTab : VisibilityContainer { 7 | [Resolved] 8 | protected RurusettoOverlay Overlay { get; private set; } = null!; 9 | [Resolved] 10 | protected RurusettoAPI API { get; private set; } = null!; 11 | [Resolved] 12 | protected RulesetDownloader Downloader { get; private set; } = null!; 13 | [Resolved] 14 | protected APIRulesetStore Rulesets { get; private set; } = null!; 15 | [Resolved] 16 | protected APIUserStore Users { get; private set; } = null!; 17 | 18 | public OverlayTab () { 19 | RelativeSizeAxes = Axes.X; 20 | AutoSizeAxes = Axes.Y; 21 | 22 | Origin = Anchor.TopCentre; 23 | Anchor = Anchor.TopCentre; 24 | } 25 | 26 | protected override void LoadComplete () { 27 | base.LoadComplete(); 28 | 29 | LoadContent(); 30 | } 31 | 32 | protected override void PopIn () { 33 | this.FadeIn( 200 ).ScaleTo( 1, 300, Easing.Out ); 34 | } 35 | 36 | protected override void PopOut () { 37 | this.FadeOut( 200 ).ScaleTo( 0.8f, 300, Easing.Out ); 38 | } 39 | 40 | protected override bool StartHidden => true; 41 | 42 | protected abstract void LoadContent (); 43 | protected virtual void OnContentLoaded () { } 44 | 45 | public override bool AcceptsFocus => true; 46 | public override bool RequestsFocus => true; 47 | protected override bool OnKeyDown ( KeyDownEvent e ) { 48 | if ( e.Key is Key.F5 ) { // NOTE o!f doenst seem to have a 'refresh' action 49 | return Refresh(); 50 | } 51 | 52 | return false; 53 | } 54 | 55 | public virtual bool Refresh () { 56 | return false; 57 | } 58 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/RequestFailedDrawable.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Textures; 2 | using osu.Framework.Localisation; 3 | using osu.Framework.Platform; 4 | using osu.Game.Graphics.Backgrounds; 5 | using osu.Game.Graphics.Sprites; 6 | using osu.Game.Graphics.UserInterface; 7 | using osu.Game.Overlays; 8 | 9 | namespace osu.Game.Rulesets.RurusettoAddon.UI; 10 | 11 | public class RequestFailedDrawable : CompositeDrawable { 12 | public RequestFailedDrawable () { 13 | header = new OsuSpriteText { 14 | Font = OsuFont.GetFont( size: 45, weight: FontWeight.Bold ), 15 | Origin = Anchor.BottomCentre, 16 | Anchor = Anchor.BottomCentre, 17 | Text = Localisation.Strings.ErrorHeader, 18 | Rotation = 5 19 | }; 20 | content = new OsuSpriteText { 21 | Font = OsuFont.GetFont( size: 38, weight: FontWeight.SemiBold ), 22 | Origin = Anchor.BottomCentre, 23 | Anchor = Anchor.BottomCentre, 24 | Text = Localisation.Strings.ErrorMessageGeneric 25 | }; 26 | footer = new OsuSpriteText { 27 | Font = OsuFont.GetFont( size: 14 ), 28 | Origin = Anchor.BottomCentre, 29 | Anchor = Anchor.BottomCentre, 30 | Text = Localisation.Strings.ErrorFooter 31 | }; 32 | button = new RetryButton { 33 | Margin = new MarginPadding { Top = 10 }, 34 | MinWidth = 110, 35 | Height = 38, 36 | Origin = Anchor.BottomCentre, 37 | Anchor = Anchor.BottomCentre, 38 | Text = Localisation.Strings.Retry 39 | }; 40 | } 41 | 42 | SpriteText header; 43 | SpriteText content; 44 | SpriteText footer; 45 | OsuButton button; 46 | 47 | public LocalisableString HeaderText { 48 | get => header.Text; 49 | set => header.Text = value; 50 | } 51 | public LocalisableString ContentText { 52 | get => content.Text; 53 | set => content.Text = value; 54 | } 55 | public LocalisableString FooterText { 56 | get => footer.Text; 57 | set => footer.Text = value; 58 | } 59 | public LocalisableString ButtonText { 60 | get => button.Text; 61 | set => button.Text = value; 62 | } 63 | 64 | public Action ButtonClicked { 65 | get => button.Action; 66 | set => button.Action = value; 67 | } 68 | 69 | [BackgroundDependencyLoader] 70 | private void load ( OverlayColourProvider colours, GameHost host, TextureStore textures, RurusettoAddonRuleset ruleset ) { 71 | RelativeSizeAxes = Axes.X; 72 | AutoSizeAxes = Axes.Y; 73 | 74 | Masking = true; 75 | 76 | AddInternal( new FillFlowContainer { 77 | RelativeSizeAxes = Axes.X, 78 | AutoSizeAxes = Axes.Y, 79 | Direction = FillDirection.Vertical, 80 | Children = new Drawable[] { 81 | new Container { 82 | RelativeSizeAxes = Axes.X, 83 | Height = 400, 84 | Children = new Drawable[] { 85 | new Sprite { 86 | RelativeSizeAxes = Axes.Both, 87 | Texture = ruleset.GetTexture( host, textures, TextureNames.ErrorCover ), 88 | Anchor = Anchor.BottomCentre, 89 | Origin = Anchor.BottomCentre, 90 | FillMode = FillMode.Fit, 91 | Scale = new( 1.18f ) 92 | }, 93 | new Box { 94 | RelativeSizeAxes = Axes.Both, 95 | Colour = ColourInfo.GradientVertical( colours.Background5.Opacity( 0 ), colours.Background5 ) 96 | }, 97 | new SectionTriangles { 98 | Origin = Anchor.BottomCentre, 99 | Anchor = Anchor.BottomCentre 100 | }, 101 | new FillFlowContainer { 102 | Direction = FillDirection.Vertical, 103 | Anchor = Anchor.BottomCentre, 104 | Origin = Anchor.BottomCentre, 105 | RelativeSizeAxes = Axes.X, 106 | AutoSizeAxes = Axes.Y, 107 | Y = -20, 108 | Spacing = new osuTK.Vector2 { Y = 5 }, 109 | Children = new Drawable[] { 110 | button, 111 | footer, 112 | content, 113 | header 114 | } 115 | } 116 | } 117 | }, 118 | new Box { 119 | RelativeSizeAxes = Axes.X, 120 | Height = 14, 121 | Colour = colours.Background6 122 | } 123 | } 124 | } ); 125 | 126 | Margin = new MarginPadding { Bottom = 8 }; 127 | } 128 | 129 | private class SectionTriangles : BufferedContainer { 130 | private readonly Triangles triangles; 131 | 132 | public SectionTriangles () { 133 | RelativeSizeAxes = Axes.X; 134 | Height = 80; 135 | Masking = true; 136 | MaskingSmoothness = 0; 137 | Children = new Drawable[] 138 | { 139 | triangles = new Triangles 140 | { 141 | Anchor = Anchor.BottomCentre, 142 | Origin = Anchor.BottomCentre, 143 | RelativeSizeAxes = Axes.Both, 144 | TriangleScale = 3, 145 | } 146 | }; 147 | } 148 | 149 | [BackgroundDependencyLoader] 150 | private void load ( OverlayColourProvider colours ) { 151 | Colour = ColourInfo.GradientVertical( Colour4.Transparent, Colour4.White ); 152 | triangles.ColourLight = colours.Background5; 153 | triangles.ColourDark = colours.Background5.Darken( 0.2f ); 154 | } 155 | } 156 | 157 | private class RetryButton : OsuButton { 158 | SpriteText text = null!; 159 | public float MinWidth; 160 | 161 | protected override void Update () { 162 | base.Update(); 163 | Width = Math.Max( MinWidth, text.DrawWidth + 18 ); 164 | } 165 | 166 | protected override SpriteText CreateText () => text = new OsuSpriteText { 167 | Depth = -1, 168 | Origin = Anchor.Centre, 169 | Anchor = Anchor.Centre, 170 | Font = OsuFont.GetFont( size: 26, weight: FontWeight.Bold ) 171 | }; 172 | } 173 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/RulesetDownloadButton.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Extensions.EnumExtensions; 2 | using osu.Framework.Graphics.Cursor; 3 | using osu.Framework.Localisation; 4 | using osu.Game.Graphics.UserInterface; 5 | using osu.Game.Overlays; 6 | 7 | namespace osu.Game.Rulesets.RurusettoAddon.UI; 8 | 9 | public class RulesetDownloadButton : GrayButton { 10 | [Resolved] 11 | public RulesetDownloader Downloader { get; private set; } = null!; 12 | 13 | public readonly Bindable State = new( DownloadState.NotDownloading ); 14 | public readonly Bindable Avail = new( Availability.Unknown ); 15 | 16 | APIRuleset ruleset; 17 | public bool UseDarkerBackground { get; init; } 18 | public bool ProvideContextMenu { get; init; } = true; 19 | public RulesetDownloadButton ( APIRuleset ruleset ) : base( FontAwesome.Solid.Download ) { 20 | this.ruleset = ruleset; 21 | Action = onClick; 22 | } 23 | 24 | [Resolved] 25 | private OsuColour colours { get; set; } = null!; 26 | 27 | [Resolved] 28 | private OverlayColourProvider overlayColours { get; set; } = null!; 29 | 30 | LoadingSpinner spinner = null!; 31 | Warning warning = null!; 32 | protected override void LoadComplete () { 33 | base.LoadComplete(); 34 | 35 | Icon.Colour = Colour4.White; 36 | Icon.Scale = new osuTK.Vector2( 1.5f ); 37 | Background.Colour = overlayColours.Background3; 38 | 39 | Add( spinner = new LoadingSpinner { 40 | Scale = new osuTK.Vector2( 0.45f ) 41 | } ); 42 | 43 | AddInternal( warning = new Warning { 44 | Height = 16, 45 | Width = 16, 46 | Alpha = 0, 47 | X = -7, 48 | Y = -7 49 | } ); 50 | 51 | if ( ProvideContextMenu ) 52 | AddInternal( new RulesetManagementContextMenu( ruleset ) ); 53 | 54 | Downloader.BindWith( ruleset, State ); 55 | Downloader.BindWith( ruleset, Avail ); 56 | 57 | State.ValueChanged += _ => Schedule( updateVisuals ); 58 | Avail.ValueChanged += _ => Schedule( updateVisuals ); 59 | 60 | updateVisuals(); 61 | 62 | Schedule( () => FinishTransforms( true ) ); 63 | } 64 | 65 | [Resolved] 66 | private IBindable currentRuleset { get; set; } = null!; 67 | 68 | private void updateVisuals () { 69 | if ( State.Value == DownloadState.Downloading ) { 70 | Icon.Alpha = 0; 71 | spinner.Alpha = 1; 72 | this.FadeTo( 1f, 200 ); 73 | Background.FadeColour( colours.Blue3, 200, Easing.InOutExpo ); 74 | TooltipText = Localisation.Strings.Downloading; 75 | warning.FadeOut( 200 ); 76 | } 77 | else if ( State.Value is DownloadState.ToBeImported or DownloadState.ToBeRemoved || Avail.Value.HasFlagFast( Availability.AvailableLocally ) ) { 78 | spinner.Alpha = 0; 79 | Icon.Alpha = 1; 80 | this.FadeTo( 1f, 200 ); 81 | Background.FadeColour( Colour4.FromHex( "#6CB946" ), 200, Easing.InOutExpo ); 82 | if ( Avail.Value.HasFlagFast( Availability.Outdated ) && Avail.Value.HasFlagFast( Availability.AvailableOnline ) && State.Value == DownloadState.NotDownloading ) { 83 | Icon.Scale = new osuTK.Vector2( 1.5f ); 84 | Icon.Icon = FontAwesome.Solid.Download; 85 | 86 | TooltipText = Localisation.Strings.Update; 87 | } 88 | else { 89 | Icon.Scale = new osuTK.Vector2( 1.7f ); 90 | Icon.Icon = FontAwesome.Regular.CheckCircle; 91 | 92 | TooltipText = Avail.Value.HasFlagFast( Availability.NotAvailableOnline ) ? Localisation.Strings.InstalledUnavailableOnline : Localisation.Strings.Installed; 93 | } 94 | 95 | if ( State.Value == DownloadState.ToBeImported ) { 96 | warning.FadeIn( 200 ); 97 | warning.TooltipText = Avail.Value.HasFlagFast( Availability.AvailableLocally ) ? Localisation.Strings.ToBeUpdated : Localisation.Strings.ToBeInstalled; 98 | } 99 | else if ( State.Value == DownloadState.ToBeRemoved ) { 100 | warning.FadeIn( 200 ); 101 | warning.TooltipText = Localisation.Strings.ToBeRemoved; 102 | } 103 | else if ( Avail.Value.HasFlagFast( Availability.Outdated ) ) { 104 | warning.FadeIn( 200 ); 105 | warning.TooltipText = Localisation.Strings.Outdated; 106 | } 107 | else { 108 | warning.FadeOut( 200 ); 109 | } 110 | } 111 | else if ( Avail.Value.HasFlagFast( Availability.NotAvailableOnline ) ) { 112 | spinner.Alpha = 0; 113 | Icon.Alpha = 1; 114 | this.FadeTo( 0.6f, 200 ); 115 | Background.FadeColour( UseDarkerBackground ? overlayColours.Background4 : overlayColours.Background3, 200, Easing.InOutExpo ); 116 | TooltipText = Localisation.Strings.UnavailableOnline; 117 | warning.FadeOut( 200 ); 118 | Icon.Scale = new osuTK.Vector2( 1.5f ); 119 | Icon.Icon = FontAwesome.Solid.Download; 120 | } 121 | else if ( Avail.Value.HasFlagFast( Availability.AvailableOnline ) ) { 122 | spinner.Alpha = 0; 123 | Icon.Alpha = 1; 124 | this.FadeTo( 1f, 200 ); 125 | Background.FadeColour( UseDarkerBackground ? overlayColours.Background4 : overlayColours.Background3, 200, Easing.InOutExpo ); 126 | Icon.Scale = new osuTK.Vector2( 1.5f ); 127 | Icon.Icon = FontAwesome.Solid.Download; 128 | TooltipText = Localisation.Strings.Download; 129 | warning.FadeOut( 200 ); 130 | } 131 | 132 | if ( Avail.Value == Availability.Unknown ) { 133 | this.FadeTo( 0.6f, 200 ); 134 | TooltipText = Localisation.Strings.DownloadChecking; 135 | warning.FadeOut( 200 ); 136 | } 137 | } 138 | 139 | void onClick () { 140 | if ( Avail.Value.HasFlagFast( Availability.AvailableOnline ) && State.Value == DownloadState.NotDownloading && Avail.Value.HasFlagFast( Availability.Outdated ) ) { 141 | Downloader.UpdateRuleset( ruleset ); 142 | } 143 | else if ( Avail.Value.HasFlagFast( Availability.AvailableOnline ) && State.Value == DownloadState.NotDownloading && Avail.Value.HasFlagFast( Availability.NotAvailableLocally ) ) { 144 | Downloader.DownloadRuleset( ruleset ); 145 | } 146 | else if ( Avail.Value.HasFlagFast( Availability.AvailableLocally ) && currentRuleset is Bindable current && ruleset.LocalRulesetInfo is RulesetInfo info ) { 147 | current.Value = info; 148 | } 149 | } 150 | 151 | private class Warning : CircularContainer, IHasTooltip { 152 | public Warning () { 153 | Add( new Box { 154 | RelativeSizeAxes = Axes.Both, 155 | Colour = Colour4.FromHex( "#FF6060" ) 156 | } ); 157 | Add( new Circle { 158 | Origin = Anchor.Centre, 159 | Anchor = Anchor.Centre, 160 | RelativeSizeAxes = Axes.Both, 161 | Size = new osuTK.Vector2( 0.55f ), 162 | Colour = Colour4.White 163 | } ); 164 | 165 | Masking = true; 166 | } 167 | 168 | public LocalisableString TooltipText { get; set; } 169 | } 170 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/RulesetLogo.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Overlays; 2 | using TagLib.IFD; 3 | 4 | namespace osu.Game.Rulesets.RurusettoAddon.UI; 5 | 6 | public class RulesetLogo : CompositeDrawable { 7 | [Resolved] 8 | protected RurusettoAPI API { get; private set; } = null!; 9 | 10 | APIRuleset ruleset; 11 | public bool UseDarkerBackground { get; init; } 12 | public RulesetLogo ( APIRuleset ruleset ) { 13 | this.ruleset = ruleset; 14 | } 15 | 16 | [BackgroundDependencyLoader] 17 | private void load ( OverlayColourProvider colours ) { 18 | var color = UseDarkerBackground ? colours.Background4 : colours.Background3; 19 | 20 | InternalChildren = new Drawable[] { 21 | new Circle { 22 | RelativeSizeAxes = Axes.Both, 23 | Colour = color 24 | } 25 | }; 26 | 27 | ruleset.RequestDarkLogo( logo => { 28 | try { 29 | AddInternal( logo ); 30 | } 31 | catch { 32 | RemoveInternal( logo, false ); 33 | ruleset.RequestDarkLogo( AddInternal, AddInternal, useLocalIcon: false ); 34 | } 35 | }, fallback => AddInternal( fallback ) ); 36 | } 37 | 38 | bool subtreeWorks = true; 39 | public override bool UpdateSubTree () { 40 | if ( !subtreeWorks ) 41 | return false; 42 | 43 | try { 44 | return base.UpdateSubTree(); 45 | } 46 | catch { 47 | subtreeWorks = false; 48 | return false; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/RulesetManagementContextMenu.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Extensions.EnumExtensions; 2 | using osu.Framework.Graphics.Cursor; 3 | using osu.Framework.Graphics.UserInterface; 4 | using osu.Game.Graphics.UserInterface; 5 | using osu.Game.Rulesets.RurusettoAddon.UI.Menus; 6 | 7 | namespace osu.Game.Rulesets.RurusettoAddon.UI; 8 | 9 | public class RulesetManagementContextMenu : CompositeDrawable, IHasContextMenu { 10 | [Resolved] 11 | public RulesetDownloader Downloader { get; private set; } = null!; 12 | 13 | public readonly Bindable State = new( DownloadState.NotDownloading ); 14 | public readonly Bindable Avail = new( Availability.Unknown ); 15 | 16 | APIRuleset ruleset; 17 | LocalisableOsuMenuItem download; 18 | LocalisableOsuMenuItem update; 19 | LocalisableOsuMenuItem redownload; 20 | LocalisableOsuMenuItem remove; 21 | LocalisableOsuMenuItem cancelDownload; 22 | LocalisableOsuMenuItem cancelUpdate; 23 | LocalisableOsuMenuItem cancelRemoval; 24 | LocalisableOsuMenuItem refresh; 25 | 26 | public RulesetManagementContextMenu ( APIRuleset ruleset ) { 27 | this.ruleset = ruleset; 28 | 29 | RelativeSizeAxes = Axes.Both; 30 | 31 | download = new( Localisation.Strings.Download, MenuItemType.Standard, () => Downloader.DownloadRuleset( ruleset ) ); 32 | update = new( Localisation.Strings.Update, MenuItemType.Standard, () => Downloader.UpdateRuleset( ruleset ) ); 33 | redownload = new( Localisation.Strings.Redownload, MenuItemType.Standard, () => Downloader.UpdateRuleset( ruleset ) ); 34 | remove = new( Localisation.Strings.Remove, MenuItemType.Destructive, () => Downloader.RemoveRuleset( ruleset ) ); 35 | cancelDownload = new( Localisation.Strings.CancelDownload, MenuItemType.Standard, () => Downloader.CancelRulesetDownload( ruleset ) ); 36 | cancelUpdate = new( Localisation.Strings.CancelUpdate, MenuItemType.Standard, () => Downloader.CancelRulesetDownload( ruleset ) ); 37 | cancelRemoval = new( Localisation.Strings.CancelRemove, MenuItemType.Standard, () => Downloader.CancelRulesetRemoval( ruleset ) ); 38 | refresh = new( Localisation.Strings.Refresh, MenuItemType.Standard, () => Downloader.CheckAvailability( ruleset ) ); 39 | } 40 | 41 | protected override void LoadComplete () { 42 | base.LoadComplete(); 43 | 44 | Downloader.BindWith( ruleset, State ); 45 | Downloader.BindWith( ruleset, Avail ); 46 | 47 | State.ValueChanged += _ => Schedule( updateContextMenu ); 48 | Avail.ValueChanged += _ => Schedule( updateContextMenu ); 49 | 50 | updateContextMenu(); 51 | } 52 | 53 | private void updateContextMenu () { 54 | if ( Avail.Value == Availability.Unknown ) { 55 | ContextMenuItems = Array.Empty(); 56 | return; 57 | } 58 | 59 | if ( !ruleset.IsModifiable ) { 60 | ContextMenuItems = Array.Empty(); 61 | return; 62 | } 63 | 64 | if ( State.Value == DownloadState.Downloading ) { 65 | ContextMenuItems = Avail.Value.HasFlagFast( Availability.AvailableLocally ) 66 | ? new MenuItem[] { cancelUpdate } 67 | : new MenuItem[] { cancelDownload }; 68 | } 69 | else if ( State.Value is DownloadState.ToBeImported or DownloadState.ToBeRemoved || Avail.Value.HasFlagFast( Availability.AvailableLocally ) ) { 70 | if ( State.Value == DownloadState.ToBeImported ) { 71 | ContextMenuItems = Avail.Value.HasFlagFast( Availability.AvailableLocally ) 72 | ? new MenuItem[] { refresh, cancelUpdate } 73 | : new MenuItem[] { refresh, remove }; 74 | } 75 | else if ( State.Value == DownloadState.ToBeRemoved ) { 76 | ContextMenuItems = new MenuItem[] { refresh, cancelRemoval }; 77 | } 78 | else if ( Avail.Value.HasFlagFast( Availability.Outdated ) ) { 79 | ContextMenuItems = Avail.Value.HasFlagFast( Availability.AvailableOnline ) 80 | ? new MenuItem[] { refresh, update, remove } 81 | : new MenuItem[] { refresh, remove }; 82 | } 83 | else { 84 | ContextMenuItems = Avail.Value.HasFlagFast( Availability.AvailableOnline ) 85 | ? new MenuItem[] { refresh, redownload, remove } 86 | : new MenuItem[] { refresh, remove }; 87 | } 88 | } 89 | else if ( Avail.Value.HasFlagFast( Availability.NotAvailableOnline ) ) { 90 | ContextMenuItems = new MenuItem[] { refresh }; 91 | } 92 | else if ( Avail.Value.HasFlagFast( Availability.AvailableOnline ) ) { 93 | ContextMenuItems = new MenuItem[] { refresh, download }; 94 | } 95 | else { 96 | ContextMenuItems = Array.Empty(); 97 | } 98 | } 99 | 100 | public MenuItem[] ContextMenuItems { get; private set; } = Array.Empty(); 101 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/RurusettoAddonConfigSubsection.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Localisation; 2 | using osu.Game.Overlays.Settings; 3 | using osu.Game.Rulesets.RurusettoAddon.Configuration; 4 | 5 | namespace osu.Game.Rulesets.RurusettoAddon.UI; 6 | 7 | public class RurusettoAddonConfigSubsection : RulesetSettingsSubsection { 8 | public RurusettoAddonConfigSubsection ( Ruleset ruleset ) : base( ruleset ) { } 9 | 10 | protected override LocalisableString Header => Localisation.Strings.SettingsHeader; 11 | 12 | protected override void LoadComplete () { 13 | base.LoadComplete(); 14 | var config = (RurusettoConfigManager)Config; 15 | 16 | Add( new SettingsTextBox { LabelText = Localisation.Strings.SettingsApiAddress, Current = config.GetBindable( RurusettoSetting.APIAddress ) } ); 17 | } 18 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/TogglableScrollContainer.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Input.Events; 2 | 3 | namespace osu.Game.Rulesets.RurusettoAddon.UI; 4 | 5 | public class TogglableScrollContainer : OsuScrollContainer { 6 | public TogglableScrollContainer ( Direction direction = Direction.Vertical ) : base( direction ) { } 7 | 8 | protected bool CanScroll => ScrollDirection switch { 9 | Direction.Vertical => DrawHeight < ScrollContent.DrawHeight, 10 | _ => DrawWidth < ScrollContent.DrawWidth 11 | }; 12 | 13 | protected override bool OnMouseDown ( MouseDownEvent e ) 14 | => CanScroll && base.OnMouseDown( e ); 15 | 16 | protected override bool OnDragStart ( DragStartEvent e ) 17 | => CanScroll && base.OnDragStart( e ); 18 | 19 | public override bool DragBlocksClick => CanScroll && base.DragBlocksClick; 20 | 21 | protected override bool OnHover ( HoverEvent e ) 22 | => CanScroll && base.OnHover( e ); 23 | 24 | protected override bool OnScroll ( ScrollEvent e ) 25 | => CanScroll && base.OnScroll( e ); 26 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/Users/DrawableRurusettoUser.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Cursor; 2 | using osu.Framework.Graphics.Textures; 3 | using osu.Framework.Input.Events; 4 | using osu.Framework.Localisation; 5 | using osu.Framework.Platform; 6 | using osu.Game.Graphics.Sprites; 7 | using osu.Game.Graphics.UserInterface; 8 | using osu.Game.Online.API; 9 | using osu.Game.Online.API.Requests; 10 | using osu.Game.Overlays; 11 | 12 | #nullable disable 13 | namespace osu.Game.Rulesets.RurusettoAddon.UI.Users; 14 | 15 | public class DrawableRurusettoUser : CompositeDrawable, IHasTooltip { 16 | [Resolved] 17 | protected RurusettoOverlay Overlay { get; private set; } 18 | 19 | [Resolved( canBeNull: true )] 20 | protected UserProfileOverlay ProfileOverlay { get; private set; } 21 | [Resolved( canBeNull: true )] 22 | protected IAPIProvider OnlineAPI { get; private set; } 23 | 24 | APIUser user; 25 | Container pfpContainer; 26 | Sprite pfp; 27 | 28 | FillFlowContainer usernameFlow; 29 | LocalisableString usernameText; 30 | OsuTextFlowContainer username; 31 | FillFlowContainer verticalFlow; 32 | Drawable verifiedDrawable; 33 | UserProfile profile; 34 | bool isVerified; 35 | public bool UseDarkerBackground { get; init; } 36 | public DrawableRurusettoUser ( APIUser user, bool isVerified = false ) { 37 | this.isVerified = isVerified; 38 | this.user = user; 39 | AutoSizeAxes = Axes.X; 40 | } 41 | 42 | [Resolved] 43 | OverlayColourProvider colours { get; set; } = null!; 44 | 45 | [BackgroundDependencyLoader] 46 | private void load ( GameHost host, TextureStore textures, RurusettoAddonRuleset ruleset ) { 47 | var color = UseDarkerBackground ? colours.Background4 : colours.Background3; 48 | 49 | AddInternal( new HoverClickSounds( HoverSampleSet.Button ) ); 50 | AddInternal( usernameFlow = new FillFlowContainer { 51 | RelativeSizeAxes = Axes.Y, 52 | AutoSizeAxes = Axes.X, 53 | Direction = FillDirection.Horizontal, 54 | Children = new Drawable[] { 55 | pfpContainer = new Container { 56 | Anchor = Anchor.CentreLeft, 57 | Origin = Anchor.CentreLeft, 58 | Children = new Drawable[] { 59 | new Box { 60 | RelativeSizeAxes = Axes.Both, 61 | FillAspectRatio = 1, 62 | FillMode = FillMode.Fill, 63 | Colour = color 64 | }, 65 | pfp = new Sprite { 66 | RelativeSizeAxes = Axes.Both, 67 | FillMode = FillMode.Fit, 68 | Texture = ruleset.GetTexture( host, textures, TextureNames.DefaultAvatar ) 69 | } 70 | }, 71 | Masking = true, 72 | CornerRadius = 4, 73 | Margin = new MarginPadding { Right = 12 } 74 | }, 75 | verticalFlow = new FillFlowContainer { 76 | Direction = FillDirection.Vertical, 77 | AutoSizeAxes = Axes.Both, 78 | Anchor = Anchor.CentreLeft, 79 | Origin = Anchor.CentreLeft, 80 | Spacing = new osuTK.Vector2( 0, 4 ), 81 | Child = username = new OsuTextFlowContainer { 82 | TextAnchor = Anchor.CentreLeft, 83 | Anchor = Anchor.CentreLeft, 84 | Origin = Anchor.CentreLeft, 85 | AutoSizeAxes = Axes.Both, 86 | Margin = new MarginPadding { Right = 5 } 87 | } 88 | } 89 | } 90 | } ); 91 | 92 | makeShort(); 93 | } 94 | 95 | private bool isTall; 96 | protected override void Update () { 97 | base.Update(); 98 | 99 | pfpContainer.Width = pfpContainer.Height = DrawHeight; 100 | if ( DrawHeight > 34 && !isTall ) 101 | makeTall(); 102 | else if ( DrawHeight <= 34 && isTall ) 103 | makeShort(); 104 | } 105 | 106 | private void makeShort () { 107 | isTall = false; 108 | 109 | if ( verifiedDrawable != null ) { 110 | ( (Container)verifiedDrawable.Parent ).Remove( verifiedDrawable, false ); 111 | } 112 | 113 | if ( isVerified ) { 114 | usernameFlow.Add( verifiedDrawable = new VerifiedIcon { 115 | Anchor = Anchor.CentreLeft, 116 | Origin = Anchor.CentreLeft, 117 | Height = 15, 118 | Width = 15 119 | } ); 120 | } 121 | } 122 | 123 | private void makeTall () { 124 | isTall = true; 125 | 126 | if ( verifiedDrawable != null ) { 127 | ( (Container)verifiedDrawable.Parent )!.Remove( verifiedDrawable, false ); 128 | } 129 | 130 | if ( isVerified ) { 131 | verticalFlow.Add( verifiedDrawable = new FillFlowContainer { 132 | AutoSizeAxes = Axes.Both, 133 | Direction = FillDirection.Horizontal, 134 | Anchor = Anchor.CentreLeft, 135 | Origin = Anchor.CentreLeft, 136 | Children = new Drawable[] { 137 | new VerifiedIcon { 138 | Anchor = Anchor.CentreLeft, 139 | Origin = Anchor.CentreLeft, 140 | Height = 15, 141 | Width = 15 142 | }, 143 | new OsuSpriteText { 144 | Colour = colours.Colour1, 145 | Text = Localisation.Strings.CreatorVerified, 146 | Font = OsuFont.GetFont( weight: FontWeight.Bold ), 147 | Anchor = Anchor.CentreLeft, 148 | Origin = Anchor.CentreLeft, 149 | Margin = new MarginPadding { Left = 5 } 150 | } 151 | } 152 | } ); 153 | } 154 | } 155 | 156 | protected override void LoadComplete () { 157 | base.LoadComplete(); 158 | 159 | user.RequestDetail( profile => { 160 | this.profile = profile; 161 | usernameText = profile.Username ?? ""; 162 | username.Text = profile.Username ?? Localisation.Strings.UserUnknown; 163 | } ); 164 | 165 | user.RequestProfilePicture( texture => { 166 | pfp.Texture = texture; 167 | } ); 168 | } 169 | 170 | public LocalisableString TooltipText => usernameText; 171 | 172 | protected override bool OnClick ( ClickEvent e ) { 173 | if ( !string.IsNullOrWhiteSpace( profile?.OsuUsername ) && ProfileOverlay != null && OnlineAPI != null ) { 174 | var request = new GetUserRequest( profile.OsuUsername ); 175 | request.Success += v => { 176 | ProfileOverlay.ShowUser( v ); 177 | }; 178 | request.Failure += v => { 179 | // :( 180 | }; 181 | OnlineAPI.PerformAsync( request ); 182 | } 183 | 184 | //if ( user.HasProfile ) { 185 | // user.RequestDetail( profile => Overlay.Header.NavigateTo( user, profile.Username, perserveCategories: true ) ); 186 | //} 187 | 188 | return true; 189 | } 190 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/Users/UserTab.cs: -------------------------------------------------------------------------------- 1 | namespace osu.Game.Rulesets.RurusettoAddon.UI.Users; 2 | 3 | public class UserTab : OverlayTab { 4 | public readonly APIUser User; 5 | public UserTab ( APIUser user ) { 6 | User = user; 7 | } 8 | 9 | protected override void LoadContent () { 10 | Add( new DrawableRurusettoUser( User, false ) { Height = 80 } ); 11 | 12 | User.RequestDetail( profile => { 13 | OnContentLoaded(); 14 | }, failure: e => { 15 | API.LogFailure( $"Could not retrieve profile for {User}", e ); 16 | OnContentLoaded(); 17 | } ); 18 | } 19 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/Users/VerifiedIcon.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Cursor; 2 | using osu.Framework.Input.Events; 3 | using osu.Framework.Localisation; 4 | using osu.Game.Overlays; 5 | using osuTK.Graphics; 6 | 7 | namespace osu.Game.Rulesets.RurusettoAddon.UI.Users; 8 | 9 | public class VerifiedIcon : CompositeDrawable, IHasTooltip { 10 | SpriteIcon icon = null!; 11 | 12 | [BackgroundDependencyLoader] 13 | private void load ( OverlayColourProvider colours ) { 14 | AddInternal( icon = new SpriteIcon { 15 | Icon = FontAwesome.Solid.Certificate, 16 | Colour = colours.Colour1, 17 | RelativeSizeAxes = Axes.Both 18 | } ); 19 | } 20 | 21 | protected override bool OnHover ( HoverEvent e ) { 22 | icon.FlashColour( Color4.White, 600 ); 23 | 24 | return true; 25 | } 26 | 27 | public LocalisableString TooltipText => Localisation.Strings.CreatorVerified; 28 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/Wiki/HomeButton.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Graphics.UserInterface; 2 | 3 | namespace osu.Game.Rulesets.RurusettoAddon.UI.Wiki; 4 | 5 | public class HomeButton : GrayButton { 6 | RulesetDetail entry; 7 | public HomeButton ( RulesetDetail entry ) : base( FontAwesome.Solid.Home ) { 8 | this.entry = entry; 9 | TooltipText = Localisation.Strings.HomePage; 10 | } 11 | 12 | [BackgroundDependencyLoader( permitNulls: true )] 13 | private void load ( OsuGame game, OsuColour colours ) { 14 | Background.Colour = colours.Blue3; 15 | Icon.Colour = Colour4.White; 16 | Icon.Scale = new osuTK.Vector2( 1.5f ); 17 | 18 | if ( !string.IsNullOrWhiteSpace( entry.Source ) ) { 19 | Action = () => { 20 | game?.OpenUrlExternally( entry.Source ); 21 | }; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/Wiki/IssueButton.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Graphics.UserInterface; 2 | 3 | namespace osu.Game.Rulesets.RurusettoAddon.UI.Wiki; 4 | 5 | public class IssueButton : GrayButton { 6 | RulesetDetail entry; 7 | public IssueButton ( RulesetDetail entry ) : base( FontAwesome.Solid.Exclamation ) { 8 | this.entry = entry; 9 | TooltipText = Localisation.Strings.ReportIssue; 10 | } 11 | 12 | [BackgroundDependencyLoader( permitNulls: true )] 13 | private void load ( OsuGame game ) { 14 | Background.Colour = Colour4.FromHex( "#FF6060" ); 15 | Icon.Colour = Colour4.White; 16 | Icon.Scale = new osuTK.Vector2( 1.2f ); 17 | 18 | if ( !string.IsNullOrWhiteSpace( entry.Source ) && entry.Source.StartsWith( "https://github.com/" ) ) { 19 | Action = () => { 20 | game?.OpenUrlExternally( entry.Source.TrimEnd( '/' ) + "/issues/new" ); 21 | }; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/Wiki/MarkdownPage.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Graphics.Containers.Markdown; 2 | 3 | namespace osu.Game.Rulesets.RurusettoAddon.UI.Wiki; 4 | 5 | public class MarkdownPage : WikiPage { 6 | ContentMarkdown? content; 7 | public MarkdownPage ( APIRuleset ruleset ) : base( ruleset ) { } 8 | 9 | string text = ""; 10 | public string Text { 11 | get => text; 12 | set { 13 | text = value; 14 | if ( content != null ) 15 | content.Text = value; 16 | } 17 | } 18 | 19 | public override bool Refresh () { 20 | ClearInternal(); 21 | 22 | var address = API.GetEndpoint( Ruleset.Slug is null ? "/rulesets" : $"/rulesets/{Ruleset.Slug}" ).AbsoluteUri; 23 | AddInternal( content = new ContentMarkdown( address ) { 24 | RelativeSizeAxes = Axes.X, 25 | AutoSizeAxes = Axes.Y, 26 | Text = text 27 | } ); 28 | 29 | return true; 30 | } 31 | 32 | private class ContentMarkdown : OsuMarkdownContainer { 33 | public ContentMarkdown ( string address ) { 34 | DocumentUrl = address; 35 | var uri = new Uri( address ); 36 | RootUrl = $"{uri.Scheme}://{uri.Host}"; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/Wiki/RecommendedBeatmapsPage.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Beatmaps; 2 | using osu.Game.Beatmaps.Drawables.Cards; 3 | using osu.Game.Graphics.Containers.Markdown; 4 | using osu.Game.Online.API; 5 | using osu.Game.Online.API.Requests; 6 | using osu.Game.Online.API.Requests.Responses; 7 | using osu.Game.Rulesets.RurusettoAddon.UI.Users; 8 | 9 | namespace osu.Game.Rulesets.RurusettoAddon.UI.Wiki; 10 | 11 | public class RecommendedBeatmapsPage : WikiPage { 12 | public RecommendedBeatmapsPage ( APIRuleset ruleset ) : base( ruleset ) { } 13 | 14 | public override bool Refresh () { 15 | ClearInternal(); 16 | Ruleset.FlushRecommendations(); 17 | loadRecommendedPage(); 18 | 19 | return true; 20 | } 21 | 22 | [Resolved] 23 | protected IAPIProvider OnlineAPI { get; private set; } = null!; 24 | 25 | private void loadRecommendedPage () { 26 | Overlay.StartLoading( Tab ); 27 | 28 | // this is here because if we added to internal instead, if we refresh we could add things twice 29 | var container = new Container { 30 | RelativeSizeAxes = Axes.X, 31 | AutoSizeAxes = Axes.Y 32 | }; 33 | AddInternal( container ); 34 | 35 | Ruleset.RequestRecommendations( RurusettoAPI.RecommendationSource.All, r => { 36 | var all = new ReverseChildIDFillFlowContainer { 37 | Direction = FillDirection.Vertical, 38 | RelativeSizeAxes = Axes.X, 39 | AutoSizeAxes = Axes.Y, 40 | Spacing = new( 10 ) 41 | }; 42 | container.Child = all; 43 | 44 | int loadedCount = 0; 45 | foreach ( var group in r.GroupBy( x => x.Recommender.ID!.Value ).OrderBy( x => x.Key == Ruleset.Owner?.ID ? 1 : 2 ).ThenByDescending( x => x.Count() ) ) { 46 | var list = new ReverseChildIDFillFlowContainer { 47 | Direction = FillDirection.Full, 48 | RelativeSizeAxes = Axes.X, 49 | AutoSizeAxes = Axes.Y, 50 | Spacing = new( 10 ), 51 | Anchor = Anchor.TopCentre, 52 | Origin = Anchor.TopCentre, 53 | Margin = new() { Bottom = 20 } 54 | }; 55 | all.Add( new GridContainer { 56 | RelativeSizeAxes = Axes.X, 57 | Width = 0.9f, 58 | Height = 30, 59 | Anchor = Anchor.TopCentre, 60 | Origin = Anchor.TopCentre, 61 | ColumnDimensions = new Dimension[] { 62 | new(), 63 | new( GridSizeMode.AutoSize ), 64 | new() 65 | }, 66 | Content = new Drawable[][] { 67 | new Drawable[] { 68 | new Circle { 69 | Height = 3, 70 | RelativeSizeAxes = Axes.X, 71 | Colour = ColourProvider.Colour1, 72 | Anchor = Anchor.Centre, 73 | Origin = Anchor.Centre 74 | }, 75 | new DrawableRurusettoUser( Users.GetUser( group.Key ), group.Key == Ruleset.Owner?.ID ) { 76 | Anchor = Anchor.Centre, 77 | Origin = Anchor.Centre, 78 | Height = 30, 79 | Margin = new MarginPadding { Horizontal = 10 } 80 | }, 81 | new Circle { 82 | Height = 3, 83 | RelativeSizeAxes = Axes.X, 84 | Colour = ColourProvider.Colour1, 85 | Anchor = Anchor.Centre, 86 | Origin = Anchor.Centre 87 | }, 88 | } 89 | } 90 | } ); 91 | all.Add( list ); 92 | 93 | void add ( BeatmapRecommendation i, APIBeatmapSet v ) { 94 | v.Beatmaps = v.Beatmaps.Where( x => x.DifficultyName == i.Version ).ToArray(); 95 | 96 | list.Add( new ReverseChildIDFillFlowContainer { 97 | AutoSizeAxes = Axes.Both, 98 | Direction = FillDirection.Vertical, 99 | Anchor = Anchor.TopCentre, 100 | Origin = Anchor.TopCentre, 101 | Children = new Drawable[] { 102 | new BeatmapCardNormal( v ), 103 | new FillFlowContainer { 104 | AutoSizeAxes = Axes.Y, 105 | RelativeSizeAxes = Axes.X, 106 | Direction = FillDirection.Horizontal, 107 | Margin = new() { Top = 5 }, 108 | Children = new Drawable[] { 109 | new SpriteIcon { 110 | Icon = FontAwesome.Solid.QuoteLeft, 111 | Size = new Vector2( 34, 18 ) * 0.6f 112 | }, 113 | new OsuMarkdownContainer { 114 | AutoSizeAxes = Axes.Y, 115 | RelativeSizeAxes = Axes.X, 116 | Text = i.Comment, 117 | Margin = new() { Left = 4 - 38 * 0.6f } 118 | } 119 | } 120 | } 121 | } 122 | } ); 123 | } 124 | 125 | foreach ( var i in group.OrderByDescending( x => x.CreatedAt ) ) { 126 | var request = new GetBeatmapSetRequest( i.BeatmapID, BeatmapSetLookupType.BeatmapId ); 127 | 128 | request.Success += v => { 129 | add( i, v ); 130 | 131 | if ( ++loadedCount == r.Count ) { 132 | Overlay.FinishLoadiong( Tab ); 133 | } 134 | }; 135 | void onFail ( Exception e ) { 136 | add( i, new() { 137 | Artist = i.Artist, 138 | ArtistUnicode = i.Artist, 139 | BPM = i.BPM, 140 | Title = i.Title, 141 | TitleUnicode = i.Title, 142 | AuthorString = i.Creator, 143 | Status = (BeatmapOnlineStatus)i.Status, 144 | Beatmaps = new APIBeatmap[] { 145 | new() { 146 | OnlineID = i.BeatmapID, 147 | OnlineBeatmapSetID = i.BeatmapSetID, 148 | RulesetID = 1, 149 | BPM = i.BPM, 150 | StarRating = i.StarDifficulty 151 | } 152 | } 153 | } ); 154 | 155 | if ( ++loadedCount == r.Count ) { 156 | Overlay.FinishLoadiong( Tab ); 157 | } 158 | } 159 | request.Failure += onFail; 160 | 161 | if ( OnlineAPI is DummyAPIAccess ) 162 | onFail( null! ); 163 | else 164 | OnlineAPI.PerformAsync( request ); 165 | } 166 | } 167 | }, failure: e => { 168 | container.Child = new RequestFailedDrawable { 169 | ContentText = Localisation.Strings.ErrorMessageGeneric 170 | // TODO retry 171 | }; 172 | API.LogFailure( $"Could not retrieve recommendations for {Ruleset}", e ); 173 | Overlay.FinishLoadiong( Tab ); 174 | } ); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/Wiki/WikiPage.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Overlays; 2 | 3 | namespace osu.Game.Rulesets.RurusettoAddon.UI.Wiki; 4 | 5 | public abstract class WikiPage : CompositeDrawable { 6 | [Resolved] 7 | protected WikiTab Tab { get; private set; } = null!; 8 | [Resolved] 9 | protected RurusettoOverlay Overlay { get; private set; } = null!; 10 | [Resolved] 11 | protected RurusettoAPI API { get; private set; } = null!; 12 | [Resolved] 13 | protected OverlayColourProvider ColourProvider { get; private set; } = null!; 14 | [Resolved] 15 | protected APIUserStore Users { get; private set; } = null!; 16 | 17 | protected readonly APIRuleset Ruleset; 18 | 19 | public WikiPage ( APIRuleset ruleset ) { 20 | RelativeSizeAxes = Axes.X; 21 | AutoSizeAxes = Axes.Y; 22 | Ruleset = ruleset; 23 | } 24 | 25 | public abstract bool Refresh (); 26 | 27 | protected override void LoadComplete () { 28 | base.LoadComplete(); 29 | Refresh(); 30 | } 31 | } -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/UI/Wiki/WikiSubpage.cs: -------------------------------------------------------------------------------- 1 | namespace osu.Game.Rulesets.RurusettoAddon.UI.Wiki; 2 | 3 | public class WikiSubpage : WikiPage { 4 | string slug; 5 | public WikiSubpage ( APIRuleset ruleset, string slug ) : base( ruleset ) { 6 | this.slug = slug; 7 | } 8 | 9 | public override bool Refresh () { 10 | ClearInternal(); 11 | 12 | Ruleset.FlushSubpage( slug ); 13 | var content = new FillFlowContainer { 14 | RelativeSizeAxes = Axes.X, 15 | AutoSizeAxes = Axes.Y, 16 | Direction = FillDirection.Vertical 17 | }; 18 | AddInternal( content ); 19 | 20 | Overlay.StartLoading( Tab ); 21 | Ruleset.RequestSubpage( slug, subpage => { 22 | var markdown = new MarkdownPage( Ruleset ) { Text = subpage.Content ?? "" }; 23 | content.Child = markdown; 24 | Overlay.FinishLoadiong( Tab ); 25 | 26 | }, failure: e => { 27 | content.Add( new Container { 28 | Padding = new MarginPadding { Horizontal = -32 }, 29 | AutoSizeAxes = Axes.Y, 30 | RelativeSizeAxes = Axes.X, 31 | Child = new RequestFailedDrawable { 32 | ContentText = Localisation.Strings.PageFetchError, 33 | ButtonClicked = () => Refresh() 34 | } 35 | } ); 36 | 37 | Overlay.FinishLoadiong( Tab ); 38 | } ); 39 | 40 | return true; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/osu.Game.Rulesets.RurusettoAddon.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | osu.Game.Rulesets.Sample 5 | Library 6 | AnyCPU 7 | osu.Game.Rulesets.RurusettoAddon 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.RurusettoAddon/osu.Game.Rulesets.RurusettoAddon.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | 6 | -------------------------------------------------------------------------------- /overlayButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flutterish/rurusetto-addon/47c7d97a249e8d4423360988dfc33655d775060e/overlayButton.png --------------------------------------------------------------------------------