├── .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  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