├── .gitignore ├── CollectionDowngrader.cs ├── CollectionDowngrader.csproj ├── CollectionDowngrader.sln ├── LICENSE ├── LazerSchema ├── Beatmap.cs ├── BeatmapCollection.cs ├── BeatmapDifficulty.cs ├── BeatmapMetadata.cs ├── BeatmapSet.cs ├── BeatmapUserSettings.cs ├── RealmFile.cs ├── RealmNamedFileUsage.cs ├── RealmUser.cs └── Ruleset.cs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /obj/ 3 | /packages/ 4 | /riderModule.iml 5 | /_ReSharper.Caches/ 6 | /FodyWeavers* 7 | /collection.db 8 | -------------------------------------------------------------------------------- /CollectionDowngrader.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using Realms; 3 | using Realms.Exceptions; 4 | using CollectionDowngrader.LazerSchema; 5 | 6 | namespace CollectionDowngrader 7 | { 8 | class CollectionDowngrader 9 | { 10 | const int LazerSchemaVersion = 41; 11 | 12 | private static int Main(string[] args) 13 | { 14 | String realmFile, outputFile; 15 | FileStream outStream; 16 | BinaryWriter binWriter; 17 | Realm db; 18 | DateTimeOffset lastMod; 19 | BeatmapCollection? lastModCollection; 20 | 21 | if (args.Length != 2) 22 | { 23 | Console.Error.WriteLine("Usage: CollectionDowngrader " + 24 | ""); 25 | 26 | return 1; 27 | } 28 | 29 | realmFile = args[0]; 30 | outputFile = args[1]; 31 | 32 | if (File.Exists(realmFile)) { 33 | Console.WriteLine("Found realm file."); 34 | } else { 35 | Console.Error.WriteLine("Realm file does not exist, stop."); 36 | 37 | return 1; 38 | } 39 | 40 | if (File.Exists(outputFile)) 41 | { 42 | Console.Error.WriteLine("Output file already exists, aborting."); 43 | 44 | return 1; 45 | } 46 | 47 | RealmConfiguration config = new(realmFile) 48 | { 49 | IsReadOnly = true, 50 | SchemaVersion = LazerSchemaVersion 51 | }; 52 | 53 | config.Schema = new[] { 54 | typeof(Beatmap), 55 | typeof(BeatmapCollection), 56 | typeof(BeatmapDifficulty), 57 | typeof(BeatmapMetadata), 58 | typeof(BeatmapSet), 59 | typeof(BeatmapUserSettings), 60 | typeof(RealmFile), 61 | typeof(RealmNamedFileUsage), 62 | typeof(RealmUser), 63 | typeof(Ruleset), 64 | typeof(ModPreset) 65 | }; 66 | 67 | try 68 | { 69 | db = Realm.GetInstance(config); 70 | } 71 | catch (RealmException re) 72 | { 73 | Console.Error.WriteLine($"Error opening database:\n\n{re.Message}"); 74 | 75 | // example msg: "Provided schema version A does not equal last set version B." 76 | // example msg2: "Provided schema version A is less than last set version B." 77 | 78 | if (re.Message.Contains("less than last set version")) 79 | { 80 | Console.Error.WriteLine("It seemed like the specified osu! (lazer) database is in a new format " + 81 | "which is not compatible with this version of CollectionDowngrader."); 82 | Console.Error.WriteLine("\nYou can go check the project page to see if there's a new release, " + 83 | "or file an Issue on GitHub to let me know it needs updating."); 84 | } 85 | else 86 | { 87 | Regex regex = new Regex(@"Provided schema version (\d+) does not equal last set version (\d+)."); 88 | 89 | Match match = regex.Match(re.Message); 90 | 91 | if (match.Success && match.Groups.Count == 3) 92 | { 93 | int providedVersion = int.Parse(match.Groups[1].Value); 94 | int lastSetVersion = int.Parse(match.Groups[2].Value); 95 | 96 | // if provided version is smaller than the last set version, it means CollectionDowngrader is too old 97 | // ask the user to update in this case 98 | 99 | if (providedVersion < lastSetVersion) 100 | { 101 | Console.Error.WriteLine("It seemed like the specified osu! (lazer) database is in a new format " + 102 | "which is not compatible with this version of CollectionDowngrader."); 103 | Console.Error.WriteLine("\nYou can go check the project page to see if there's a new release, " + 104 | "or file an Issue on GitHub to let me know it needs updating."); 105 | } 106 | else 107 | { 108 | // otherwise, user installed CollectionDowngrader is too new for the database 109 | // ask the user to update osu! (lazer) client or downgrade CollectionDowngrader in this case 110 | 111 | Console.Error.WriteLine("It seemed like the specified osu! (lazer) database is in an old format " + 112 | "which is not compatible with this version of CollectionDowngrader."); 113 | Console.Error.WriteLine("\nYou can try to update your osu! (lazer) client, or downgrade " + 114 | "CollectionDowngrader to an older version."); 115 | } 116 | } 117 | } 118 | 119 | return 1; 120 | } 121 | 122 | Console.WriteLine("The specified osu! (lazer) database is loaded successfully."); 123 | 124 | List collections = db.All().ToList(); 125 | int collectionCount = collections.Count; 126 | 127 | Console.WriteLine($"Found {collectionCount} collections in the database."); 128 | 129 | try 130 | { 131 | outStream = File.Open(outputFile, FileMode.CreateNew, FileAccess.Write); 132 | } 133 | catch (IOException ioe) 134 | { 135 | Console.Error.WriteLine($"Cannot create output file for writing: {ioe.Message}"); 136 | 137 | db.Dispose(); 138 | return 1; 139 | } 140 | 141 | Console.WriteLine("Output file is created successfully, now start writing data."); 142 | 143 | binWriter = new BinaryWriter(outStream); 144 | 145 | // find the last modified collection and its modification date 146 | lastModCollection = collections.MaxBy(i => i.LastModified.Ticks); 147 | lastMod = lastModCollection is null ? DateTimeOffset.Now : lastModCollection.LastModified; 148 | 149 | try 150 | { 151 | 152 | binWriter.Write((int)lastMod.Ticks); // last modification date 153 | binWriter.Write(collectionCount); // collection count 154 | 155 | foreach (BeatmapCollection collection in collections) 156 | { 157 | binWriter.Write((byte)0x0b); 158 | binWriter.Write(collection.Name); // collection name 159 | binWriter.Write(collection.BeatmapMD5Hashes.Count); // beatmap count 160 | 161 | foreach (string hash in collection.BeatmapMD5Hashes) 162 | { 163 | binWriter.Write((byte)0x0b); 164 | binWriter.Write(hash); // beatmap MD5 hash 165 | } 166 | } 167 | } 168 | catch (IOException ioe) 169 | { 170 | Console.Error.WriteLine($"Error writing data: {ioe.Message}"); 171 | 172 | binWriter.Close(); 173 | outStream.Close(); 174 | db.Dispose(); 175 | return 1; 176 | } 177 | 178 | binWriter.Close(); 179 | outStream.Close(); 180 | db.Dispose(); 181 | 182 | Console.WriteLine("Everything is OK."); 183 | 184 | return 0; 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /CollectionDowngrader.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /CollectionDowngrader.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CollectionDowngrader", "CollectionDowngrader.csproj", "{F97772E9-AF01-45FB-928D-8A1F7B7A828D}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {F97772E9-AF01-45FB-928D-8A1F7B7A828D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {F97772E9-AF01-45FB-928D-8A1F7B7A828D}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {F97772E9-AF01-45FB-928D-8A1F7B7A828D}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {F97772E9-AF01-45FB-928D-8A1F7B7A828D}.Release|Any CPU.Build.0 = Release|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2022 kabii 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /LazerSchema/Beatmap.cs: -------------------------------------------------------------------------------- 1 | // Original source file (modified by kabii) Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. 2 | using Realms; 3 | 4 | namespace CollectionDowngrader.LazerSchema 5 | { 6 | public class Beatmap : RealmObject 7 | { 8 | [PrimaryKey] 9 | public Guid ID { get; set; } = Guid.NewGuid(); 10 | 11 | public string DifficultyName { get; set; } = string.Empty; 12 | 13 | public Ruleset Ruleset { get; set; } = null!; 14 | 15 | public BeatmapDifficulty Difficulty { get; set; } = null!; 16 | 17 | public BeatmapMetadata Metadata { get; set; } = null!; 18 | 19 | public BeatmapUserSettings UserSettings { get; set; } = null!; 20 | 21 | public BeatmapSet? BeatmapSet { get; set; } 22 | 23 | public int Status { get; set; } 24 | 25 | [Indexed] 26 | public int OnlineID { get; set; } = -1; 27 | 28 | public double Length { get; set; } 29 | 30 | public double BPM { get; set; } 31 | 32 | public string Hash { get; set; } = string.Empty; 33 | 34 | public double StarRating { get; set; } = -1; 35 | 36 | [Indexed] 37 | public string MD5Hash { get; set; } = string.Empty; 38 | 39 | public string OnlineMD5Hash { get; set; } = string.Empty; 40 | 41 | public DateTimeOffset? LastLocalUpdate { get; set; } 42 | 43 | public DateTimeOffset? LastOnlineUpdate { get; set; } 44 | 45 | public bool Hidden { get; set; } 46 | 47 | public double AudioLeadIn { get; set; } 48 | 49 | public float StackLeniency { get; set; } = 0.7f; 50 | 51 | public bool SpecialStyle { get; set; } 52 | 53 | public bool LetterboxInBreaks { get; set; } 54 | 55 | public bool WidescreenStoryboard { get; set; } 56 | 57 | public bool EpilepsyWarning { get; set; } 58 | 59 | public bool SamplesMatchPlaybackRate { get; set; } 60 | 61 | public DateTimeOffset? LastPlayed { get; set; } 62 | 63 | public double DistanceSpacing { get; set; } 64 | 65 | public int BeatDivisor { get; set; } 66 | 67 | public int GridSize { get; set; } 68 | 69 | public double TimelineZoom { get; set; } 70 | 71 | public double? EditorTimestamp { get; set; } 72 | 73 | public int CountdownOffset { get; set; } 74 | 75 | // Author kabii 76 | public override bool Equals(object? obj) 77 | { 78 | if (obj == null || !GetType().Equals(obj.GetType())) 79 | { 80 | return false; 81 | } 82 | else 83 | { 84 | Beatmap map = (Beatmap)obj; 85 | return ID == map.ID; 86 | } 87 | } 88 | 89 | public override int GetHashCode() 90 | { 91 | return HashCode.Combine(base.GetHashCode(), ID); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /LazerSchema/BeatmapCollection.cs: -------------------------------------------------------------------------------- 1 | // Original source file (modified by kabii) Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. 2 | using Realms; 3 | 4 | namespace CollectionDowngrader.LazerSchema 5 | { 6 | public class BeatmapCollection : RealmObject 7 | { 8 | [PrimaryKey] 9 | public Guid ID { get; set; } 10 | 11 | public string Name { get; set; } = string.Empty; 12 | 13 | public IList BeatmapMD5Hashes { get; } = null!; 14 | 15 | public DateTimeOffset LastModified { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LazerSchema/BeatmapDifficulty.cs: -------------------------------------------------------------------------------- 1 | // Original source file (modified by kabii) Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. 2 | using Realms; 3 | 4 | namespace CollectionDowngrader.LazerSchema 5 | { 6 | public class BeatmapDifficulty : EmbeddedObject 7 | { 8 | public float DrainRate { get; set; } = 0.0f; 9 | public float CircleSize { get; set; } = 0.0f; 10 | public float OverallDifficulty { get; set; } = 0.0f; 11 | public float ApproachRate { get; set; } = 0.0f; 12 | 13 | public double SliderMultiplier { get; set; } = 1; 14 | public double SliderTickRate { get; set; } = 1; 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LazerSchema/BeatmapMetadata.cs: -------------------------------------------------------------------------------- 1 | // Original source file (modified by kabii) Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. 2 | using Realms; 3 | 4 | namespace CollectionDowngrader.LazerSchema 5 | { 6 | public class BeatmapMetadata : RealmObject 7 | { 8 | public string Title { get; set; } = string.Empty; 9 | public string TitleUnicode { get; set; } = string.Empty; 10 | public string Artist { get; set; } = string.Empty; 11 | public string ArtistUnicode { get; set; } = string.Empty; 12 | public RealmUser Author { get; set; } = new RealmUser(); 13 | public string Source { get; set; } = string.Empty; 14 | public string? Tags { get; set; } = string.Empty; 15 | public int PreviewTime { get; set; } 16 | public string AudioFile { get; set; } = string.Empty; 17 | public string BackgroundFile { get; set; } = string.Empty; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /LazerSchema/BeatmapSet.cs: -------------------------------------------------------------------------------- 1 | // Original source file (modified by kabii) Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. 2 | using Realms; 3 | using System.Text; 4 | 5 | namespace CollectionDowngrader.LazerSchema 6 | { 7 | public class BeatmapSet : RealmObject 8 | { 9 | [PrimaryKey] 10 | public Guid ID { get; set; } = Guid.NewGuid(); 11 | [Indexed] 12 | public int OnlineID { get; set; } = -1; 13 | public DateTimeOffset DateAdded { get; set; } 14 | public DateTimeOffset? DateSubmitted { get; set; } 15 | public DateTimeOffset? DateRanked { get; set; } 16 | public IList Beatmaps { get; } = null!; 17 | public IList Files { get; } = null!; 18 | public int Status { get; set; } = -3; 19 | public bool DeletePending { get; set; } 20 | public string Hash { get; set; } = string.Empty; 21 | public bool Protected { get; set; } 22 | 23 | // Author kabii 24 | IList? selected = null; 25 | 26 | [Ignored] 27 | public IList SelectedBeatmaps 28 | { 29 | get 30 | { 31 | return selected switch 32 | { 33 | not null => selected, 34 | null => Beatmaps 35 | }; 36 | } 37 | set { selected = value; } 38 | } 39 | 40 | public string Display() 41 | { 42 | BeatmapMetadata metadata = Beatmaps.First().Metadata; 43 | var difficulties = SelectedBeatmaps.Select(b => b.StarRating).OrderBy(r => r).Select(r => r.ToString("0.00")); 44 | string difficultySpread = string.Join(", ", difficulties); 45 | 46 | var output = new StringBuilder(); 47 | output 48 | .Append(OnlineID) 49 | .Append(": ") 50 | .Append(metadata.Artist) 51 | .Append(" - ") 52 | .Append(metadata.Title) 53 | .Append(" (") 54 | .Append(metadata.Author.Username) 55 | .Append(" - ") 56 | .Append(difficultySpread) 57 | .Append(" stars)"); 58 | return output.ToString(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /LazerSchema/BeatmapUserSettings.cs: -------------------------------------------------------------------------------- 1 | using Realms; 2 | 3 | namespace CollectionDowngrader.LazerSchema 4 | { 5 | public class BeatmapUserSettings : EmbeddedObject 6 | { 7 | public double Offset { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LazerSchema/RealmFile.cs: -------------------------------------------------------------------------------- 1 | // Original source file (modified by kabii) Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. 2 | using Realms; 3 | 4 | namespace CollectionDowngrader.LazerSchema 5 | { 6 | [MapTo("File")] 7 | public class RealmFile : RealmObject 8 | { 9 | [PrimaryKey] 10 | public string Hash { get; set; } = string.Empty; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /LazerSchema/RealmNamedFileUsage.cs: -------------------------------------------------------------------------------- 1 | // Original source file (modified by kabii) Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. 2 | using Realms; 3 | 4 | namespace CollectionDowngrader.LazerSchema 5 | { 6 | public class RealmNamedFileUsage : EmbeddedObject 7 | { 8 | public RealmFile File { get; set; } = null!; 9 | public string Filename { get; set; } = null!; 10 | 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /LazerSchema/RealmUser.cs: -------------------------------------------------------------------------------- 1 | // Original source file (modified by kabii) Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. 2 | using Realms; 3 | 4 | namespace CollectionDowngrader.LazerSchema 5 | { 6 | public class RealmUser : EmbeddedObject 7 | { 8 | public int OnlineID { get; set; } = 1; 9 | public string Username { get; set; } = string.Empty; 10 | public string? CountryCode { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /LazerSchema/Ruleset.cs: -------------------------------------------------------------------------------- 1 | // Original source file (modified by kabii) Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. 2 | using Realms; 3 | 4 | namespace CollectionDowngrader.LazerSchema 5 | { 6 | public class Ruleset : RealmObject 7 | { 8 | [PrimaryKey] 9 | public string ShortName { get; set; } = string.Empty; 10 | [Indexed] 11 | public int OnlineID { get; set; } = -1; 12 | public string Name { get; set; } = string.Empty; 13 | public string InstantiationInfo { get; set; } = string.Empty; 14 | public int LastAppliedDifficultyVersion { get; set; } 15 | public bool Available { get; set; } 16 | } 17 | 18 | public class ModPreset : RealmObject 19 | { 20 | [PrimaryKey] 21 | public Guid ID { get; set; } = Guid.NewGuid(); 22 | public Ruleset Ruleset { get; set; } = null!; 23 | public string Name { get; set; } = string.Empty; 24 | public string Description { get; set; } = string.Empty; 25 | public string Mods { get; set; } = string.Empty; 26 | public bool DeletePending { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CollectionDowngrader 2 | ========================= 3 | 4 | > [!WARNING] 5 | > 6 | > This operation (Importing data from lazer to stable) is not officially supported by osu!, so use this at your own risk and don't forget to backup your data before downgrading! 7 | 8 | A very simple and tiny program that converts osu! (lazer) collection data to osu! (stable) collection.db format. 9 | 10 | ## Usage 11 | 12 | > [!NOTE] 13 | > 14 | > You should already know where your osu! installations are located and what you are trying to do. 15 | 16 | `CollectionDowngrader.exe ` 17 | 18 | ### Example 19 | 20 | This command reads the collection data from osu! (lazer) at the default installation path and creates a collection.db 21 | that can be used in osu! (stable). 22 | 23 | ````shell 24 | CollectionDowngrader.exe %APPDATA%/osu/client.realm collection.db 25 | ```` 26 | 27 | > [!WARNING] 28 | > 29 | > If you already have some collections in your osu! (stable) installation, then you should ***NEVER*** directly overwrite 30 | the existing collection.db with a new one. Instead, use [CollectionManager][CollectionManager] for merging data in multiple collection databases. 31 | 32 | ## Downloads 33 | 34 | [GitHub Releases](../../releases) 35 | 36 | ## How to build 37 | 38 | Just utilize your IDEs. 39 | 40 | ## Credits / See also 41 | 42 | * [kabiiQ/BeatmapExporter][BeatmapExporter]: Inspiration of this project, all code for osu! (lazer) realm schema and 43 | the database parsing. 44 | * [Piotrekol/CollectionManager][CollectionManager]: Code for creating osu! (stable) collection.db files. 45 | 46 | [BeatmapExporter]: https://github.com/kabiiQ/BeatmapExporter.git 47 | [CollectionManager]: https://github.com/Piotrekol/CollectionManager.git 48 | --------------------------------------------------------------------------------