diff --git a/README.md b/README.md index 4e5b829..766c97f 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,20 @@ +[![CodeFactor](https://www.codefactor.io/repository/github/holly-hacker/osu-database-reader/badge)](https://www.codefactor.io/repository/github/holly-hacker/osu-database-reader) # osu-database-reader Allows for parsing/reading osu!'s database files In the future, this will read more file formats, and likely write them too. +NOTE: No parsing for storyboards, use [osuElements](https://github.com/JasperDeSutter/osuElements) for that. + ### Currently finished: * reading osu!.db * reading collection.db * reading scores.db * reading presence.db * reading replays +* reading osu! beatmaps ### Planned: -* reading osu! beatmaps (maybe?) * writing osu!.db * writing collection.db * writing scores.db diff --git a/UnitTestProject/SharedCode.cs b/UnitTestProject/SharedCode.cs new file mode 100644 index 0000000..a2422e5 --- /dev/null +++ b/UnitTestProject/SharedCode.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTestProject +{ + public static class SharedCode + { + public static readonly string PathOsu; + + static SharedCode() + { + //find osu! installation + if (!Directory.Exists(PathOsu = $@"C:\Users\{Environment.UserName}\AppData\Local\osu!\") //current install dir + && !Directory.Exists(PathOsu = @"C:\Program Files (x86)\osu!\") //old install dir (x64-based) + && !Directory.Exists(PathOsu = @"C:\Program Files\osu!\")) { //old install dir (x86-based) + PathOsu = string.Empty; //if none of the previous paths exist + } + //TODO: allow dev to create file with custom location + } + + public static void PreTestCheck() + { + if (string.IsNullOrEmpty(PathOsu)) + Assert.Inconclusive("osu! installation directory not found."); + } + + public static string GetRelativeFile(string rel, bool shouldError = false) + { + string absolute = Path.Combine(PathOsu, rel); + if (!File.Exists(absolute)) { + if (shouldError) + Assert.Fail($"File does not exist: {absolute}"); + else + Assert.Inconclusive($"File does not exist: {absolute}"); + } + return absolute; + } + + public static string GetRelativeDirectory(string rel, bool shouldError = false) + { + string absolute = Path.Combine(PathOsu, rel); + if (!Directory.Exists(absolute)) + { + if (shouldError) + Assert.Fail($"Directory does not exist: {absolute}"); + else + Assert.Inconclusive($"Directory does not exist: {absolute}"); + } + return absolute; + } + + public static bool VerifyFileChecksum(string path, string md5) + { + string hash = string.Join(string.Empty, MD5.Create().ComputeHash(File.OpenRead(path)).Select(a => a.ToString("X2"))); + return hash == md5.ToUpper(); + } + } +} diff --git a/UnitTestProject/TestsBinary.cs b/UnitTestProject/TestsBinary.cs new file mode 100644 index 0000000..09ca827 --- /dev/null +++ b/UnitTestProject/TestsBinary.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using osu_database_reader.BinaryFiles; +using osu_database_reader.Components.Player; + +namespace UnitTestProject +{ + [TestClass] + public class TestsBinary + { + [TestInitialize] + public void Init() + { + SharedCode.PreTestCheck(); + } + + [TestMethod] + public void ReadOsuDb() + { + OsuDb db = OsuDb.Read(SharedCode.GetRelativeFile("osu!.db")); + Debug.WriteLine("Version: " + db.OsuVersion); + Debug.WriteLine("Amount of beatmaps: " + db.Beatmaps.Count); + Debug.WriteLine($"Account name: \"{db.AccountName}\" (account {(db.AccountUnlocked ? "not locked" : "locked, unlocked at " + db.AccountUnlockDate)})"); + Debug.WriteLine("Account rank: " + db.AccountRank); + for (int i = 0; i < Math.Min(10, db.Beatmaps.Count); i++) { //print 10 at most + var b = db.Beatmaps[i]; + Debug.WriteLine($"{b.Artist} - {b.Title} [{b.Version}]"); + } + } + + [TestMethod] + public void ReadCollectionDb() + { + CollectionDb db = CollectionDb.Read(SharedCode.GetRelativeFile("collection.db")); + Debug.WriteLine("Version: " + db.OsuVersion); + Debug.WriteLine("Amount of collections: " + db.Collections.Count); + foreach (var c in db.Collections) { + Debug.WriteLine($" - Collection {c.Name} has {c.BeatmapHashes.Count} item" + (c.BeatmapHashes.Count == 1 ? "" : "s")); + } + } + + [TestMethod] + public void ReadScoresDb() + { + ScoresDb db = ScoresDb.Read(SharedCode.GetRelativeFile("scores.db")); + Debug.WriteLine("Version: " + db.OsuVersion); + Debug.WriteLine("Amount of beatmaps: " + db.Beatmaps.Count); + Debug.WriteLine("Amount of scores: " + db.Scores.Count()); + + string[] keys = db.Beatmaps.Keys.ToArray(); + for (int i = 0; i < Math.Min(25, keys.Length); i++) { //print 25 at most + string md5 = keys[i]; + List replays = db.Beatmaps[md5]; + + Debug.WriteLine($"Beatmap with md5 {md5} has replays:"); + for (int j = 0; j < Math.Min(5, replays.Count); j++) { //again, 5 at most + var r = replays[j]; + Debug.WriteLine($"\tReplay by {r.PlayerName}, for {r.Score} score with {r.Combo}x combo. Played at {r.TimePlayed}"); + } + } + } + + [TestMethod] + public void ReadPresenceDb() + { + var db = PresenceDb.Read(SharedCode.GetRelativeFile("presence.db")); + Debug.WriteLine("Version: " + db.OsuVersion); + Debug.WriteLine("Amount of scores: " + db.Players.Count); + + for (int i = 0; i < Math.Min(db.Players.Count, 10); i++) { //10 at most + var p = db.Players[i]; + Debug.WriteLine($"Player {p.PlayerName} lives at long {p.Longitude} and lat {p.Latitude}. Some DateTime: {p.Unknown1}. Rank: {p.PlayerRank}. {p.GameMode}, #{p.GlobalRank}, id {p.PlayerId}"); + } + } + + [TestMethod] + public void ReadReplay() + { + //get random file + string path = SharedCode.GetRelativeDirectory("Replays"); + string[] files = Directory.GetFiles(path); + + if (files.Length == 0) + Assert.Inconclusive("No replays in your replay folder!"); + + for (int i = 0; i < Math.Min(files.Length, 10); i++) { //10 at most + var r = Replay.Read(files[i]); + Debug.WriteLine("Version: " + r.OsuVersion); + Debug.WriteLine("Beatmap hash: " + r.BeatmapHash); + Debug.WriteLine($"Replay by {r.PlayerName}, for {r.Score} score with {r.Combo}x combo. Played at {r.TimePlayed}"); + Debug.WriteLine(""); + } + } + } +} diff --git a/UnitTestProject/TestsCombined.cs b/UnitTestProject/TestsCombined.cs new file mode 100644 index 0000000..7414160 --- /dev/null +++ b/UnitTestProject/TestsCombined.cs @@ -0,0 +1,45 @@ +using System; +using System.Diagnostics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using osu_database_reader.BinaryFiles; +using osu_database_reader.TextFiles; + +namespace UnitTestProject +{ + [TestClass] + public class TestsCombined + { + [TestInitialize] + public void Init() + { + SharedCode.PreTestCheck(); + } + + [TestMethod] + public void CheckBeatmapsAgainstDb() + { + OsuDb db = OsuDb.Read(SharedCode.GetRelativeFile("osu!.db")); + + for (var i = 0; i < Math.Min(db.Beatmaps.Count, 50); i++) { + var entry = db.Beatmaps[i]; + + Debug.WriteLine($"Going to read beatmap at /{entry.FolderName}/{entry.BeatmapFileName}"); + + //read beatmap + BeatmapFile bm = BeatmapFile.Read(SharedCode.GetRelativeFile($"Songs/{entry.FolderName}/{entry.BeatmapFileName}", true)); + //BUG: this can still fail when maps use the hold note (used in some mania maps?) + + Assert.IsTrue(bm.SectionGeneral.Count >= 2); //disco prince only has 2 + Assert.IsTrue(bm.SectionMetadata.Count >= 4); //disco prince only has 4 + Assert.IsTrue(bm.SectionDifficulty.Count >= 5); //disco prince only has 5 + + Assert.AreEqual(entry.Artist, bm.Artist); + Assert.AreEqual(entry.Version, bm.Version); + Assert.AreEqual(entry.Creator, bm.Creator); + Assert.AreEqual(entry.Title, bm.Title); + + //TODO: more, but check if the entries are present in the beatmap + } + } + } +} diff --git a/UnitTestProject/TestsText.cs b/UnitTestProject/TestsText.cs new file mode 100644 index 0000000..a1df249 --- /dev/null +++ b/UnitTestProject/TestsText.cs @@ -0,0 +1,108 @@ +using System; +using System.IO; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using osu_database_reader; +using osu_database_reader.Components.HitObjects; +using osu_database_reader.TextFiles; + +namespace UnitTestProject +{ + [TestClass] + public class TestsText + { + [TestInitialize] + public void Init() + { + SharedCode.PreTestCheck(); + } + + [TestMethod] + public void VerifyBigBlack() + { + //most people should have this map + string beatmap = SharedCode.GetRelativeFile(@"Songs\41823 The Quick Brown Fox - The Big Black\The Quick Brown Fox - The Big Black (Blue Dragon) [WHO'S AFRAID OF THE BIG BLACK].osu"); + + if (!File.Exists(beatmap)) + Assert.Inconclusive("Hardcoded path does not exist: " + beatmap); + if (!SharedCode.VerifyFileChecksum(beatmap, "2D687E5EE79F3862AD0C60651471CDCC")) + Assert.Inconclusive("Beatmap was modified."); + + var bm = BeatmapFile.Read(beatmap); + + Assert.AreEqual(bm.FileFormatVersion, 9); + + //check General + Assert.AreEqual(bm.SectionGeneral["AudioFilename"], "02 The Big Black.mp3"); + Assert.AreEqual(bm.SectionGeneral["AudioLeadIn"], "0"); + Assert.AreEqual(bm.SectionGeneral["PreviewTime"], "18957"); + + //check Metadata + Assert.AreEqual(bm.Title, "The Big Black"); + Assert.AreEqual(bm.Artist, "The Quick Brown Fox"); + Assert.AreEqual(bm.Creator, "Blue Dragon"); + Assert.AreEqual(bm.Version, "WHO'S AFRAID OF THE BIG BLACK"); + Assert.AreEqual(bm.Source, string.Empty); + Assert.IsTrue(Enumerable.SequenceEqual(bm.Tags, new[] { "Onosakihito", "speedcore", "renard", "lapfox" })); + + //check Difficulty + Assert.AreEqual(bm.HPDrainRate, 5f); + Assert.AreEqual(bm.CircleSize, 4f); + Assert.AreEqual(bm.OverallDifficulty, 7f); + Assert.AreEqual(bm.ApproachRate, 10f); + Assert.AreEqual(bm.SliderMultiplier, 1.8f); + Assert.AreEqual(bm.SliderTickRate, 2f); + + //check Events + //TODO + + //check TimingPoints + Assert.AreEqual(bm.TimingPoints.Count, 5); + Assert.AreEqual(bm.TimingPoints[0].Time, 6966); + Assert.AreEqual(bm.TimingPoints[1].Kiai, true); + Assert.AreEqual(bm.TimingPoints[2].MsPerQuarter, -100); //means no timing change + Assert.AreEqual(bm.TimingPoints[3].TimingChange, false); + + //check Colours + //Combo1 : 249,91,9 + //(...) + //Combo5 : 255,255,128 + Assert.AreEqual(bm.SectionColours["Combo1"], "249,91,91"); + Assert.AreEqual(bm.SectionColours["Combo5"], "255,255,128"); + Assert.AreEqual(bm.SectionColours.Count, 5); + + //check HitObjects + Assert.AreEqual(bm.HitObjects.Count, 746); + Assert.AreEqual(bm.HitObjects.Count(a => a is HitObjectCircle), 410); + Assert.AreEqual(bm.HitObjects.Count(a => a is HitObjectSlider), 334); + Assert.AreEqual(bm.HitObjects.Count(a => a is HitObjectSpinner), 2); + + //56,56,6966,1,4 + HitObjectCircle firstCircle = (HitObjectCircle) bm.HitObjects.First(a => a.Type.HasFlag(HitObjectType.Normal)); + Assert.AreEqual(firstCircle.X, 56); + Assert.AreEqual(firstCircle.Y, 56); + Assert.AreEqual(firstCircle.Time, 6966); + Assert.AreEqual(firstCircle.HitSound, HitSound.Finish); + + //178,50,7299,2,0,B|210:0|300:0|332:50,1,180,2|0 + HitObjectSlider firstSlider = (HitObjectSlider)bm.HitObjects.First(a => a.Type.HasFlag(HitObjectType.Slider)); + Assert.AreEqual(firstSlider.X, 178); + Assert.AreEqual(firstSlider.Y, 50); + Assert.AreEqual(firstSlider.Time, 7299); + Assert.AreEqual(firstSlider.HitSound, HitSound.None); + Assert.AreEqual(firstSlider.CurveType, CurveType.Bezier); + Assert.AreEqual(firstSlider.Points.Count, 3); + Assert.AreEqual(firstSlider.RepeatCount, 1); + Assert.AreEqual(firstSlider.Length, 180); + + //256,192,60254,12,4,61587 + HitObjectSpinner firstSpinner = (HitObjectSpinner)bm.HitObjects.First(a => a.Type.HasFlag(HitObjectType.Spinner)); + Assert.AreEqual(firstSpinner.X, 256); + Assert.AreEqual(firstSpinner.Y, 192); + Assert.AreEqual(firstSpinner.Time, 60254); + Assert.AreEqual(firstSpinner.HitSound, HitSound.Finish); + Assert.AreEqual(firstSpinner.EndTime, 61587); + Assert.IsTrue(firstSpinner.IsNewCombo); + } + } +} diff --git a/UnitTestProject/UnitTest1.cs b/UnitTestProject/UnitTest1.cs deleted file mode 100644 index d92b4cb..0000000 --- a/UnitTestProject/UnitTest1.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using osu_database_reader; - -namespace UnitTestProject -{ - [TestClass] - public class UnitTest1 - { - private static readonly string OsuPath; - - static UnitTest1() { - OsuPath = $@"C:\Users\{Environment.UserName}\AppData\Local\osu!\"; - } - - [TestMethod] - public void ReadOsuDb() { - OsuDb db = OsuDb.Read(OsuPath + "osu!.db"); - Debug.WriteLine("Version: " + db.OsuVersion); - Debug.WriteLine("Amount of beatmaps: " + db.AmountOfBeatmaps); - Debug.WriteLine($"Account name: {db.AccountName} (account {(db.AccountUnlocked ? "unlocked" : "locked, unlocked at "+db.AccountUnlockDate)})"); - Debug.WriteLine("Account rank: " + db.AccountRank); - for (int i = 0; i < Math.Min(10, db.AmountOfBeatmaps); i++) { //print 10 at most - var b = db.Beatmaps[i]; - Debug.WriteLine($"{b.Artist} - {b.Title} [{b.Difficulty}]"); - } - } - - [TestMethod] - public void ReadCollectionDb() { - CollectionDb db = CollectionDb.Read(OsuPath + "collection.db"); - Debug.WriteLine("Version: " + db.OsuVersion); - Debug.WriteLine("Amount of collections: " + db.AmountOfCollections); - foreach (var c in db.Collections) - Debug.WriteLine($" - Collection {c.Name} with {c.Md5Hashes.Count} item" + (c.Md5Hashes.Count == 1 ? "" : "s")); - } - - [TestMethod] - public void ReadScoresDb() { - ScoresDb db = ScoresDb.Read(OsuPath + "scores.db"); - Debug.WriteLine("Version: " + db.OsuVersion); - Debug.WriteLine("Amount of beatmaps: " + db.AmountOfBeatmaps); - Debug.WriteLine("Amount of scores: " + db.AmountOfScores); - - string[] keys = db.Beatmaps.Keys.ToArray(); - for (int i = 0; i < Math.Min(25, db.AmountOfBeatmaps); i++) { //print 25 at most - string md5 = keys[i]; - List replays = db.Beatmaps[md5]; - - Debug.WriteLine($"Beatmap with md5 {md5} has replays:"); - for (int j = 0; j < Math.Min(5, replays.Count); j++) { //again, 5 at most - var r = replays[j]; - Debug.WriteLine($"\tReplay by {r.PlayerName}, for {r.Score} score with {r.Combo}x combo. Played at {r.TimePlayed}"); - } - } - } - - [TestMethod] - public void ReadPresenceDb() { - var db = PresenceDb.Read(OsuPath + "presence.db"); - Debug.WriteLine("Version: " + db.OsuVersion); - Debug.WriteLine("Amount of scores: " + db.AmountOfPlayers); - - for (int i = 0; i < Math.Min(db.AmountOfPlayers, 10); i++) { //10 at most - var p = db.Players[i]; - Debug.WriteLine($"Player {p.PlayerName} lives at long {p.Longitude} and lat {p.Latitude}. Some DateTime: {p.Unknown1}. Rank: {p.PlayerRank}. {p.GameMode}, #{p.GlobalRank}, id {p.PlayerId}"); - } - } - - [TestMethod] - public void ReadReplay() { - //get random path - string path = OsuPath + "Replays\\"; - string[] files = Directory.GetFiles(path); - - Assert.IsNotNull(files); - Assert.AreNotEqual(files.Length, 0, "No replays in your replay folder!"); - - for (int i = 0; i < Math.Max(files.Length, 10); i++) { //10 at most - var r = Replay.Read(files[i]); - Debug.WriteLine("Version: " + r.OsuVersion); - Debug.WriteLine("Beatmap hash: " + r.BeatmapHash); - Debug.WriteLine($"Replay by {r.PlayerName}, for {r.Score} score with {r.Combo}x combo. Played at {r.TimePlayed}"); - Debug.WriteLine(""); - } - } - } -} diff --git a/UnitTestProject/UnitTestProject.csproj b/UnitTestProject/UnitTestProject.csproj index f940f3b..281ccf1 100644 --- a/UnitTestProject/UnitTestProject.csproj +++ b/UnitTestProject/UnitTestProject.csproj @@ -50,8 +50,11 @@ - + + + + diff --git a/osu-database-reader.sln.DotSettings b/osu-database-reader.sln.DotSettings new file mode 100644 index 0000000..46afbfd --- /dev/null +++ b/osu-database-reader.sln.DotSettings @@ -0,0 +1,5 @@ + + AR + CS + HP + OD \ No newline at end of file diff --git a/osu-database-reader/CollectionDb.cs b/osu-database-reader/BinaryFiles/CollectionDb.cs similarity index 60% rename from osu-database-reader/CollectionDb.cs rename to osu-database-reader/BinaryFiles/CollectionDb.cs index 899dd2f..d45841d 100644 --- a/osu-database-reader/CollectionDb.cs +++ b/osu-database-reader/BinaryFiles/CollectionDb.cs @@ -1,30 +1,27 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using osu_database_reader.Components.Beatmaps; +using osu_database_reader.IO; -namespace osu_database_reader +namespace osu_database_reader.BinaryFiles { public class CollectionDb { public int OsuVersion; - public int AmountOfCollections => Collections.Count; public List Collections = new List(); public static CollectionDb Read(string path) { var db = new CollectionDb(); - using (CustomReader r = new CustomReader(File.OpenRead(path))) { + using (CustomBinaryReader r = new CustomBinaryReader(File.OpenRead(path))) { db.OsuVersion = r.ReadInt32(); int amount = r.ReadInt32(); for (int i = 0; i < amount; i++) { var c = new Collection(); - c.Md5Hashes = new List(); + c.BeatmapHashes = new List(); c.Name = r.ReadString(); int amount2 = r.ReadInt32(); - for (int j = 0; j < amount2; j++) c.Md5Hashes.Add(r.ReadString()); + for (int j = 0; j < amount2; j++) c.BeatmapHashes.Add(r.ReadString()); db.Collections.Add(c); } diff --git a/osu-database-reader/OsuDb.cs b/osu-database-reader/BinaryFiles/OsuDb.cs similarity index 82% rename from osu-database-reader/OsuDb.cs rename to osu-database-reader/BinaryFiles/OsuDb.cs index 3ce0779..606fce1 100644 --- a/osu-database-reader/OsuDb.cs +++ b/osu-database-reader/BinaryFiles/OsuDb.cs @@ -2,8 +2,10 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using osu_database_reader.Components.Beatmaps; +using osu_database_reader.IO; -namespace osu_database_reader +namespace osu_database_reader.BinaryFiles { public class OsuDb { @@ -12,13 +14,12 @@ public class OsuDb public bool AccountUnlocked; public DateTime AccountUnlockDate; public string AccountName; - public int AmountOfBeatmaps => Beatmaps.Count; public List Beatmaps; public PlayerRank AccountRank; public static OsuDb Read(string path) { OsuDb db = new OsuDb(); - using (CustomReader r = new CustomReader(File.OpenRead(path))) { + using (CustomBinaryReader r = new CustomBinaryReader(File.OpenRead(path))) { db.OsuVersion = r.ReadInt32(); db.FolderCount = r.ReadInt32(); db.AccountUnlocked = r.ReadBoolean(); @@ -31,9 +32,8 @@ public static OsuDb Read(string path) { int currentIndex = (int)r.BaseStream.Position; int entryLength = r.ReadInt32(); - var entry = BeatmapEntry.ReadFromReader(r, false, db.OsuVersion); + db.Beatmaps.Add(BeatmapEntry.ReadFromReader(r, false, db.OsuVersion)); - db.Beatmaps.Add(entry); if (r.BaseStream.Position != currentIndex + entryLength + 4) { Debug.Fail($"Length doesn't match, {r.BaseStream.Position} instead of expected {currentIndex + entryLength + 4}"); } diff --git a/osu-database-reader/BinaryFiles/PresenceDb.cs b/osu-database-reader/BinaryFiles/PresenceDb.cs new file mode 100644 index 0000000..3402d1c --- /dev/null +++ b/osu-database-reader/BinaryFiles/PresenceDb.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.IO; +using osu_database_reader.Components.Player; +using osu_database_reader.IO; + +namespace osu_database_reader.BinaryFiles +{ + public class PresenceDb + { + public int OsuVersion; + public List Players = new List(); + + public static PresenceDb Read(string path) { + var db = new PresenceDb(); + using (CustomBinaryReader r = new CustomBinaryReader(File.OpenRead(path))) { + db.OsuVersion = r.ReadInt32(); + int amount = r.ReadInt32(); + + for (int i = 0; i < amount; i++) { + db.Players.Add(PlayerPresence.ReadFromReader(r)); + } + } + + return db; + } + } +} diff --git a/osu-database-reader/ScoresDb.cs b/osu-database-reader/BinaryFiles/ScoresDb.cs similarity index 71% rename from osu-database-reader/ScoresDb.cs rename to osu-database-reader/BinaryFiles/ScoresDb.cs index d1b533e..70929bb 100644 --- a/osu-database-reader/ScoresDb.cs +++ b/osu-database-reader/BinaryFiles/ScoresDb.cs @@ -2,21 +2,20 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; -using System.Threading.Tasks; +using osu_database_reader.Components.Player; +using osu_database_reader.IO; -namespace osu_database_reader +namespace osu_database_reader.BinaryFiles { public class ScoresDb { public int OsuVersion; public Dictionary> Beatmaps = new Dictionary>(); - public int AmountOfBeatmaps => Beatmaps.Count; - public int AmountOfScores => Beatmaps.Sum(a => a.Value.Count); + public IEnumerable Scores => Beatmaps.SelectMany(a => a.Value); public static ScoresDb Read(string path) { var db = new ScoresDb(); - using (CustomReader r = new CustomReader(File.OpenRead(path))) + using (CustomBinaryReader r = new CustomBinaryReader(File.OpenRead(path))) { db.OsuVersion = r.ReadInt32(); int amount = r.ReadInt32(); @@ -27,8 +26,9 @@ public static ScoresDb Read(string path) { Tuple> tuple = new Tuple>(md5, new List()); int amount2 = r.ReadInt32(); - for (int j = 0; j < amount2; j++) + for (int j = 0; j < amount2; j++) { tuple.Item2.Add(Replay.ReadFromReader(r, true)); + } db.Beatmaps.Add(tuple.Item1, tuple.Item2); } diff --git a/osu-database-reader/BeatmapEntry.cs b/osu-database-reader/Components/Beatmaps/BeatmapEntry.cs similarity index 73% rename from osu-database-reader/BeatmapEntry.cs rename to osu-database-reader/Components/Beatmaps/BeatmapEntry.cs index 28629ee..03c74ab 100644 --- a/osu-database-reader/BeatmapEntry.cs +++ b/osu-database-reader/Components/Beatmaps/BeatmapEntry.cs @@ -1,27 +1,27 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; +using osu_database_reader.IO; -namespace osu_database_reader +namespace osu_database_reader.Components.Beatmaps { public class BeatmapEntry { public string Artist, ArtistUnicode; public string Title, TitleUnicode; public string Creator; //mapper - public string Difficulty; //called "Version" in the .osu format + public string Version; //difficulty name public string AudioFileName; public string BeatmapChecksum; public string BeatmapFileName; public SubmissionStatus RankedStatus; public ushort CountHitCircles, CountSliders, CountSpinners; public DateTime LastModifiedTime; - public float DiffAR, DiffCS, DiffHP, DiffOD; + public float ApproachRate, CircleSize, HPDrainRate, OveralDifficulty; public double SliderVelocity; public Dictionary DiffStarRatingStandard, DiffStarRatingTaiko, DiffStarRatingCtB, DiffStarRatingMania; - public int DrainTimeSeconds; //NOTE: in s - public int TotalTime; //NOTE: in ms + public int DrainTimeSeconds; //NOTE: in s + public int TotalTime; //NOTE: in ms public int AudioPreviewTime; //NOTE: in ms public List TimingPoints; public int BeatmapId, BeatmapSetId, ThreadId; @@ -36,13 +36,13 @@ public class BeatmapEntry public DateTime LastPlayed; public bool IsOsz2; public string FolderName; - public DateTime LastCheckAgainstOsuRepo; //wtf + public DateTime LastCheckAgainstOsuRepo; public bool IgnoreBeatmapSounds, IgnoreBeatmapSkin, DisableStoryBoard, DisableVideo, VisualOverride; public short OldUnknown1; //unused public int Unknown2; public byte ManiaScrollSpeed; - public static BeatmapEntry ReadFromReader(CustomReader r, bool readLength = true, int version = 20160729) { + public static BeatmapEntry ReadFromReader(CustomBinaryReader r, bool readLength = true, int version = 20160729) { BeatmapEntry e = new BeatmapEntry(); int length = 0; @@ -54,36 +54,30 @@ public static BeatmapEntry ReadFromReader(CustomReader r, bool readLength = true e.Title = r.ReadString(); e.TitleUnicode = r.ReadString(); e.Creator = r.ReadString(); - e.Difficulty = r.ReadString(); + e.Version = r.ReadString(); e.AudioFileName = r.ReadString(); e.BeatmapChecksum = r.ReadString(); //always 32 in length, so the 2 preceding bytes in the file are practically wasting space e.BeatmapFileName = r.ReadString(); - //Debug.WriteLine($"{e.Artist} - {e.Title} [{e.Difficulty}]"); - e.RankedStatus = (SubmissionStatus)r.ReadByte(); e.CountHitCircles = r.ReadUInt16(); e.CountSliders = r.ReadUInt16(); e.CountSpinners = r.ReadUInt16(); e.LastModifiedTime = r.ReadDateTime(); - //Debug.WriteLine("Last modified: " + e.LastModifiedTime + ", ranked status is " + e.RankedStatus); - if (version >= 20140609) { - e.DiffAR = r.ReadSingle(); - e.DiffCS = r.ReadSingle(); - e.DiffHP = r.ReadSingle(); - e.DiffOD = r.ReadSingle(); + e.ApproachRate = r.ReadSingle(); + e.CircleSize = r.ReadSingle(); + e.HPDrainRate = r.ReadSingle(); + e.OveralDifficulty = r.ReadSingle(); } else { - e.DiffAR = r.ReadByte(); - e.DiffCS = r.ReadByte(); - e.DiffHP = r.ReadByte(); - e.DiffOD = r.ReadByte(); + e.ApproachRate = r.ReadByte(); + e.CircleSize = r.ReadByte(); + e.HPDrainRate = r.ReadByte(); + e.OveralDifficulty = r.ReadByte(); } - //Debug.WriteLine($"AR: {e.DiffAR} CS: {e.DiffCS} HP: {e.DiffHP} OD: {e.DiffOD}"); - e.SliderVelocity = r.ReadDouble(); if (version >= 20140609) { @@ -100,7 +94,7 @@ public static BeatmapEntry ReadFromReader(CustomReader r, bool readLength = true e.TimingPoints = r.ReadTimingPointList(); e.BeatmapId = r.ReadInt32(); e.BeatmapSetId = r.ReadInt32(); - e.ThreadId = r.ReadInt32(); //no idea what this is + e.ThreadId = r.ReadInt32(); e.GradeStandard = (Ranking)r.ReadByte(); e.GradeTaiko = (Ranking)r.ReadByte(); @@ -111,8 +105,6 @@ public static BeatmapEntry ReadFromReader(CustomReader r, bool readLength = true e.StackLeniency = r.ReadSingle(); e.GameMode = (GameMode)r.ReadByte(); - //Debug.WriteLine("gamemode: " + e.GameMode); - e.SongSource = r.ReadString(); e.SongTags = r.ReadString(); e.OffsetOnline = r.ReadInt16(); @@ -120,14 +112,10 @@ public static BeatmapEntry ReadFromReader(CustomReader r, bool readLength = true e.Unplayed = r.ReadBoolean(); e.LastPlayed = r.ReadDateTime(); - //Debug.WriteLine("Last played: " + e.LastPlayed); - e.IsOsz2 = r.ReadBoolean(); e.FolderName = r.ReadString(); e.LastCheckAgainstOsuRepo = r.ReadDateTime(); - //Debug.WriteLine("Last osu! repo check: " + e.LastCheckAgainstOsuRepo); - e.IgnoreBeatmapSounds = r.ReadBoolean(); e.IgnoreBeatmapSkin = r.ReadBoolean(); e.DisableStoryBoard = r.ReadBoolean(); @@ -138,11 +126,8 @@ public static BeatmapEntry ReadFromReader(CustomReader r, bool readLength = true e.Unknown2 = r.ReadInt32(); e.ManiaScrollSpeed = r.ReadByte(); - //Debug.WriteLine("Mania scroll speed: " + e.ManiaScrollSpeed); - int endPosition = (int) r.BaseStream.Position; - Debug.Assert(!readLength || length == endPosition - startPosition); //TODO: could throw error here - //Debug.WriteLine("---"); + Debug.Assert(!readLength || length == endPosition - startPosition); //could throw error here return e; } diff --git a/osu-database-reader/Components/Beatmaps/Collection.cs b/osu-database-reader/Components/Beatmaps/Collection.cs new file mode 100644 index 0000000..903fccc --- /dev/null +++ b/osu-database-reader/Components/Beatmaps/Collection.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace osu_database_reader.Components.Beatmaps +{ + public struct Collection + { + public string Name; + public List BeatmapHashes; + } +} diff --git a/osu-database-reader/Components/Beatmaps/TimingPoint.cs b/osu-database-reader/Components/Beatmaps/TimingPoint.cs new file mode 100644 index 0000000..d54d31e --- /dev/null +++ b/osu-database-reader/Components/Beatmaps/TimingPoint.cs @@ -0,0 +1,54 @@ +using System.IO; + +namespace osu_database_reader.Components.Beatmaps +{ + public class TimingPoint + { + public double Time, MsPerQuarter; + public bool TimingChange; + + //only in .osu files + public int? TimingSignature; // x/4 (eg. 4/4, 3/4) + public int? SampleSet; + public int? CustomSampleSet; + public int? SampleVolume; + public bool? Kiai; + + public static TimingPoint FromString(string line) + { + TimingPoint t = new TimingPoint(); + + string[] splitted = line.Split(','); + + t.Time = double.Parse(splitted[0], Constants.NumberFormat); + t.MsPerQuarter = double.Parse(splitted[1], Constants.NumberFormat); + + int temp; + + //lots of checks that are probably not needed. idc you're not my real dad. + if (splitted.Length > 2) t.TimingSignature = (temp = int.Parse(splitted[2])) == 0 ? 4 : temp; + if (splitted.Length > 3) t.SampleSet = int.Parse(splitted[3]); + if (splitted.Length > 4) t.CustomSampleSet = int.Parse(splitted[4]); + if (splitted.Length > 5) t.SampleVolume = int.Parse(splitted[5]); + if (splitted.Length > 6) t.TimingChange = int.Parse(splitted[6]) == 1; + if (splitted.Length > 7) + { + temp = int.Parse(splitted[7]); + t.Kiai = (temp & 1) != 0; + } + + return t; + } + + public static TimingPoint ReadFromReader(BinaryReader r) + { + TimingPoint t = new TimingPoint + { + MsPerQuarter = r.ReadDouble(), + Time = r.ReadDouble(), + TimingChange = r.ReadByte() != 0 + }; + return t; + } + } +} diff --git a/osu-database-reader/Components/HitObjects/HitObject.cs b/osu-database-reader/Components/HitObjects/HitObject.cs new file mode 100644 index 0000000..cc9d941 --- /dev/null +++ b/osu-database-reader/Components/HitObjects/HitObject.cs @@ -0,0 +1,66 @@ +using System; +using System.Diagnostics; + +namespace osu_database_reader.Components.HitObjects +{ + public abstract class HitObject + { + public int X, Y; //based on a 512x384 field + public int Time; + public HitObjectType Type; + public HitSound HitSound; + + public bool IsNewCombo => Type.HasFlag(HitObjectType.NewCombo); + + //automatically returns the correct type + public static HitObject FromString(string s) + { + string[] split = s.Split(','); + HitObjectType t = (HitObjectType) int.Parse(split[3], Constants.NumberFormat); + + HitObject h = null; + switch (t & (HitObjectType)0b1000_1011) { + case HitObjectType.Normal: + h = new HitObjectCircle(); + if (split.Length > 5) + (h as HitObjectCircle).SoundSampleData = split[5]; + break; + case HitObjectType.Slider: + h = new HitObjectSlider(); + (h as HitObjectSlider).ParseSliderSegments(split[5]); + (h as HitObjectSlider).RepeatCount = int.Parse(split[6], Constants.NumberFormat); + if (split.Length > 7) + (h as HitObjectSlider).Length = double.Parse(split[7], Constants.NumberFormat); + //if (split.Length > 8) + // (h as HitObjectSlider).HitSoundData = split[8]; + //if (split.Length > 9) + // (h as HitObjectSlider).SoundSampleData = split[9]; + //if (split.Length > 10) + // (h as HitObjectSlider).MoreSoundSampleData = split[10]; + break; + case HitObjectType.Spinner: + h = new HitObjectSpinner(); + (h as HitObjectSpinner).EndTime = int.Parse(split[5]); + if (split.Length > 6) + (h as HitObjectSpinner).SoundSampleData = split[6]; + break; + case HitObjectType.Hold: + throw new NotImplementedException("Hold notes are not yet parsed."); + default: + throw new ArgumentOutOfRangeException(nameof(t), "Bad hitobject type"); + } + + //note: parsed as decimal but cast to int in osu! + if (h != null) { + h.X = int.Parse(split[0], Constants.NumberFormat); + h.Y = int.Parse(split[1], Constants.NumberFormat); + h.Time = int.Parse(split[2], Constants.NumberFormat); + h.Type = t; + h.HitSound = (HitSound)int.Parse(split[4]); + } + else Debug.Fail("unhandled hitobject type"); + + return h; + } + } +} diff --git a/osu-database-reader/Components/HitObjects/HitObjectCircle.cs b/osu-database-reader/Components/HitObjects/HitObjectCircle.cs new file mode 100644 index 0000000..03f7753 --- /dev/null +++ b/osu-database-reader/Components/HitObjects/HitObjectCircle.cs @@ -0,0 +1,7 @@ +namespace osu_database_reader.Components.HitObjects +{ + public class HitObjectCircle : HitObject + { + public string SoundSampleData; //TODO: parse all sound sample data + } +} diff --git a/osu-database-reader/Components/HitObjects/HitObjectSlider.cs b/osu-database-reader/Components/HitObjects/HitObjectSlider.cs new file mode 100644 index 0000000..f1bca0e --- /dev/null +++ b/osu-database-reader/Components/HitObjects/HitObjectSlider.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace osu_database_reader.Components.HitObjects +{ + public class HitObjectSlider : HitObject + { + public CurveType CurveType; + public List Points = new List(); //does not include initial point! + public int RepeatCount; + public double Length; //seems to be length in o!p, so it doesn't have to be calculated? + + public void ParseSliderSegments(string sliderString) + { + string[] split = sliderString.Split('|'); + foreach (var s in split) { + if (s.Length == 1) { //curve type + switch (s[0]) { + case 'L': + CurveType = CurveType.Linear; + break; + case 'C': + CurveType = CurveType.Catmull; + break; + case 'P': + CurveType = CurveType.Perfect; + break; + case 'B': + CurveType = CurveType.Bezier; + break; + } + continue; + } + string[] split2 = s.Split(':'); + Debug.Assert(split2.Length == 2); + + Points.Add(new Vector2( + int.Parse(split2[0]), + int.Parse(split2[1]))); + } + } + } +} diff --git a/osu-database-reader/Components/HitObjects/HitObjectSpinner.cs b/osu-database-reader/Components/HitObjects/HitObjectSpinner.cs new file mode 100644 index 0000000..02d9599 --- /dev/null +++ b/osu-database-reader/Components/HitObjects/HitObjectSpinner.cs @@ -0,0 +1,8 @@ +namespace osu_database_reader.Components.HitObjects +{ + public class HitObjectSpinner : HitObject + { + public int EndTime; + public string SoundSampleData; + } +} diff --git a/osu-database-reader/Player.cs b/osu-database-reader/Components/Player/PlayerPresence.cs similarity index 67% rename from osu-database-reader/Player.cs rename to osu-database-reader/Components/Player/PlayerPresence.cs index 80c3960..515cd53 100644 --- a/osu-database-reader/Player.cs +++ b/osu-database-reader/Components/Player/PlayerPresence.cs @@ -1,13 +1,10 @@ -using System; -using System.Collections.Generic; +using System; using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using osu_database_reader.IO; -namespace osu_database_reader +namespace osu_database_reader.Components.Player { - public class Player + public class PlayerPresence { public int PlayerId; public string PlayerName; @@ -19,17 +16,17 @@ public class Player public int GlobalRank; public DateTime Unknown1; //TODO: name this. Last update time? - public static Player ReadFromReader(CustomReader r) { - Player p = new Player(); + public static PlayerPresence ReadFromReader(CustomBinaryReader r) { + PlayerPresence p = new PlayerPresence(); p.PlayerId = r.ReadInt32(); p.PlayerName = r.ReadString(); p.UtcOffset = r.ReadByte(); - p.CountryByte = r.ReadByte(); //TODO: turn into enum? + p.CountryByte = r.ReadByte(); //TODO: create Country enum byte b = r.ReadByte(); - p.PlayerRank = (PlayerRank) (b & -225); - p.GameMode = (GameMode)((b & 224) >> 5); + p.PlayerRank = (PlayerRank)(b & 0b0001_1111); + p.GameMode = (GameMode)((b & 0b1110_0000) >> 5); Debug.Assert((byte)p.GameMode <= 3, $"GameMode is byte {(byte)p.GameMode}, should be between 0 and 3"); p.Longitude = r.ReadSingle(); diff --git a/osu-database-reader/Replay.cs b/osu-database-reader/Components/Player/Replay.cs similarity index 71% rename from osu-database-reader/Replay.cs rename to osu-database-reader/Components/Player/Replay.cs index 00a9ada..13cb86c 100644 --- a/osu-database-reader/Replay.cs +++ b/osu-database-reader/Components/Player/Replay.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; +using System; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using osu_database_reader.IO; -namespace osu_database_reader +namespace osu_database_reader.Components.Player { public class Replay //used for both scores.db and .osr files { @@ -17,20 +14,20 @@ namespace osu_database_reader public ushort Combo; public bool FullCombo; public Mods Mods; - public string LifeGraph; //not present in scores.db, TODO: parse this when implementing .osr + public string LifeGraphData; //null in scores.db, TODO: parse this when implementing .osr public DateTime TimePlayed; - public byte[] ReplayData; //not present in scores.db - public long ScoreId; + public byte[] ReplayData; //null in scores.db + public long? ScoreId; public static Replay Read(string path) { Replay replay; - using (CustomReader r = new CustomReader(File.OpenRead(path))) { + using (CustomBinaryReader r = new CustomBinaryReader(File.OpenRead(path))) { replay = ReadFromReader(r); //scoreid should not be needed } return replay; } - public static Replay ReadFromReader(CustomReader r, bool readScoreId = false) { + public static Replay ReadFromReader(CustomBinaryReader r, bool readScoreId = false) { Replay replay = new Replay { GameMode = (GameMode) r.ReadByte(), OsuVersion = r.ReadInt32(), @@ -49,10 +46,10 @@ public static Replay ReadFromReader(CustomReader r, bool readScoreId = false) { Combo = r.ReadUInt16(), FullCombo = r.ReadBoolean(), Mods = (Mods) r.ReadInt32(), - LifeGraph = r.ReadString(), + LifeGraphData = r.ReadString(), TimePlayed = r.ReadDateTime(), ReplayData = r.ReadBytes(), - ScoreId = readScoreId ? r.ReadInt64() : -1 + ScoreId = readScoreId ? r.ReadInt64() : (long?)null }; return replay; diff --git a/osu-database-reader/Components/Vector2.cs b/osu-database-reader/Components/Vector2.cs new file mode 100644 index 0000000..ba83f6c --- /dev/null +++ b/osu-database-reader/Components/Vector2.cs @@ -0,0 +1,13 @@ +namespace osu_database_reader.Components +{ + public struct Vector2 + { + public int X, Y; + + public Vector2(int x, int y) + { + X = x; + Y = y; + } + } +} diff --git a/osu-database-reader/Constants.cs b/osu-database-reader/Constants.cs new file mode 100644 index 0000000..5d65bd1 --- /dev/null +++ b/osu-database-reader/Constants.cs @@ -0,0 +1,10 @@ +using System.Globalization; + +namespace osu_database_reader +{ + internal static class Constants + { + //nfi so parsing works on all cultures + public static readonly NumberFormatInfo NumberFormat = new CultureInfo(@"en-US", false).NumberFormat; + } +} diff --git a/osu-database-reader/Enums_Beatmaps.cs b/osu-database-reader/Enums_Beatmaps.cs new file mode 100644 index 0000000..ced5aae --- /dev/null +++ b/osu-database-reader/Enums_Beatmaps.cs @@ -0,0 +1,46 @@ +using System; + +namespace osu_database_reader +{ + [Flags] + public enum HitObjectType + { + Normal = 0b0000_0001, //I should really become consistent with these baseX styles + Slider = 0b0000_0010, + NewCombo = 0b0000_0100, + Spinner = 0b0000_1000, + ColourHax = 0b0111_0000, + Hold = 0b1000_0000, + }; + + [Flags] + public enum HitSound + { + None = 0, + Normal = 1, + Whistle = 2, + Finish = 4, + Clap = 8, + } + + public enum CurveType + { + Linear, + Catmull, + Bezier, + Perfect + } + + public enum BeatmapSection + { + _EndOfFile, + General, + Editor, + Metadata, + Difficulty, + Events, + TimingPoints, + Colours, + HitObjects, + } +} diff --git a/osu-database-reader/Enums.cs b/osu-database-reader/Enums_General.cs similarity index 95% rename from osu-database-reader/Enums.cs rename to osu-database-reader/Enums_General.cs index 1a1ab28..850602b 100644 --- a/osu-database-reader/Enums.cs +++ b/osu-database-reader/Enums_General.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace osu_database_reader { diff --git a/osu-database-reader/Extensions.cs b/osu-database-reader/Extensions.cs new file mode 100644 index 0000000..9813549 --- /dev/null +++ b/osu-database-reader/Extensions.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace osu_database_reader +{ + internal static class Extensions + { + public static string GetValueOrNull(this Dictionary dic, string key) + { + return dic.ContainsKey(key) + ? dic[key] + : null; + } + } +} diff --git a/osu-database-reader/CustomReader.cs b/osu-database-reader/IO/CustomBinaryReader.cs similarity index 51% rename from osu-database-reader/CustomReader.cs rename to osu-database-reader/IO/CustomBinaryReader.cs index 3e0c4e7..62ae8a7 100644 --- a/osu-database-reader/CustomReader.cs +++ b/osu-database-reader/IO/CustomBinaryReader.cs @@ -1,18 +1,16 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; -using System.Linq; using System.Text; -using System.Threading.Tasks; +using osu_database_reader.Components.Beatmaps; -namespace osu_database_reader +namespace osu_database_reader.IO { - public class CustomReader : BinaryReader + public class CustomBinaryReader : BinaryReader { - public CustomReader(Stream input) : base(input) {} - public CustomReader(Stream input, Encoding encoding) : base(input, encoding) {} - public CustomReader(Stream input, Encoding encoding, bool leaveOpen) : base(input, encoding, leaveOpen) {} + public CustomBinaryReader(Stream input) : base(input) {} + public CustomBinaryReader(Stream input, Encoding encoding) : base(input, encoding) {} + public CustomBinaryReader(Stream input, Encoding encoding, bool leaveOpen) : base(input, encoding, leaveOpen) {} public byte[] ReadBytes() { // an overload to ReadBytes(int count) int length = ReadInt32(); @@ -26,23 +24,23 @@ public override string ReadString() { if (b == 0x0B) return base.ReadString(); else if (b == 0x00) - return string.Empty; + return null; else - throw new Exception($"Continuation byte is not 0x00 or 0x11, but is 0x{b.ToString("X2")} (position: {BaseStream.Position})"); + throw new Exception($"Type byte is not 0x00 or 0x11, but is 0x{b:X2} (position: {BaseStream.Position})"); } public DateTime ReadDateTime() { - long idk = ReadInt64(); - return new DateTime(idk, DateTimeKind.Utc); + long ticks = ReadInt64(); + return new DateTime(ticks, DateTimeKind.Utc); } public Dictionary ReadModsDoubleDictionary() { int length = ReadInt32(); Dictionary dicks = new Dictionary(); for (int i = 0; i < length; i++) { - ReadByte(); //type (0x08) + ReadByte(); //type (0x08, Int32) int key = ReadInt32(); - ReadByte(); //type (0x0D) + ReadByte(); //type (0x0D, Double) double value = ReadDouble(); dicks.Add((Mods)key, value); } @@ -52,17 +50,8 @@ public Dictionary ReadModsDoubleDictionary() { public List ReadTimingPointList() { List list = new List(); int length = ReadInt32(); - for (int i = 0; i < length; i++) list.Add(ReadTimingPoint()); + for (int i = 0; i < length; i++) list.Add(TimingPoint.ReadFromReader(this)); return list; } - - private TimingPoint ReadTimingPoint() { - TimingPoint t = new TimingPoint { - MsPerQuarter = ReadDouble(), - Time = ReadDouble(), - NotInherited = ReadByte() != 0 - }; - return t; - } } } diff --git a/osu-database-reader/IO/CustomStreamReader.cs b/osu-database-reader/IO/CustomStreamReader.cs new file mode 100644 index 0000000..34cc775 --- /dev/null +++ b/osu-database-reader/IO/CustomStreamReader.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using osu_database_reader.Components.Beatmaps; +using osu_database_reader.Components.HitObjects; + +namespace osu_database_reader.IO +{ + public class CustomStreamReader : StreamReader + { + //automatically generated constructors + public CustomStreamReader(Stream stream) : base(stream) { } + public CustomStreamReader(Stream stream, bool detectEncodingFromByteOrderMarks) : base(stream, detectEncodingFromByteOrderMarks) { } + public CustomStreamReader(Stream stream, Encoding encoding) : base(stream, encoding) { } + public CustomStreamReader(Stream stream, Encoding encoding, bool detectEncodingFromByteOrderMarks) : base(stream, encoding, detectEncodingFromByteOrderMarks) { } + public CustomStreamReader(Stream stream, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize) : base(stream, encoding, detectEncodingFromByteOrderMarks, bufferSize) { } + public CustomStreamReader(Stream stream, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize, bool leaveOpen) : base(stream, encoding, detectEncodingFromByteOrderMarks, bufferSize, leaveOpen) { } + public CustomStreamReader(string path) : base(path) { } + public CustomStreamReader(string path, bool detectEncodingFromByteOrderMarks) : base(path, detectEncodingFromByteOrderMarks) { } + public CustomStreamReader(string path, Encoding encoding) : base(path, encoding) { } + public CustomStreamReader(string path, Encoding encoding, bool detectEncodingFromByteOrderMarks) : base(path, encoding, detectEncodingFromByteOrderMarks) { } + public CustomStreamReader(string path, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize) : base(path, encoding, detectEncodingFromByteOrderMarks, bufferSize) { } + + public BeatmapSection ReadUntilSectionStart() + { + while (!EndOfStream) { + string str = ReadLine(); + if (string.IsNullOrWhiteSpace(str)) continue; + + string stripped = str.TrimStart('[').TrimEnd(']'); + if (Enum.TryParse(stripped, out var a)) { + return a; + } + else { //oh shit + throw new Exception("Unrecognized beatmap section: " + stripped); + } + } + + //we reached an end of stream + return BeatmapSection._EndOfFile; + } + + public Dictionary ReadBasicSection(bool extraSpaceAfterColon = true, bool extraSpaceBeforeColon = false) + { + var dic = new Dictionary(); + + string line; + while (!string.IsNullOrWhiteSpace(line = ReadLine())) { + if (!line.Contains(':')) + throw new Exception("Invalid key/value line: " + line); + + int i = line.IndexOf(':'); + + string key = line.Substring(0, i); + string value = line.Substring(i + 1); + + //This is just so we can recreate files properly in the future. + //It is very likely not needed at all, but it makes me sleep + //better at night knowing everything is 100% correct. + if (extraSpaceBeforeColon && key.EndsWith(" ")) key = key.Substring(0, key.Length - 1); + if (extraSpaceAfterColon && value.StartsWith(" ")) value = value.Substring(1); + + dic.Add(key, value); + } + + return dic; + } + + public IEnumerable ReadHitObjects() + { + string line; + while (!string.IsNullOrEmpty(line = ReadLine())) { + yield return HitObject.FromString(line); + } + } + + public IEnumerable ReadTimingPoints() + { + string line; + while (!string.IsNullOrEmpty(line = ReadLine())) { + yield return TimingPoint.FromString(line); + } + } + + [Obsolete("This method should never be used; all sections must be parsed.")] + public void SkipSection() + { + while (!string.IsNullOrWhiteSpace(ReadLine())) { } + } + } +} diff --git a/osu-database-reader/PresenceDb.cs b/osu-database-reader/PresenceDb.cs deleted file mode 100644 index 3a3cbc4..0000000 --- a/osu-database-reader/PresenceDb.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace osu_database_reader -{ - public class PresenceDb - { - public int OsuVersion; - public int AmountOfPlayers => Players.Count; - public List Players = new List(); - - public static PresenceDb Read(string path) { - var db = new PresenceDb(); - using (CustomReader r = new CustomReader(File.OpenRead(path))) { - db.OsuVersion = r.ReadInt32(); - int amount = r.ReadInt32(); - - for (int i = 0; i < amount; i++) { - db.Players.Add(Player.ReadFromReader(r)); - } - } - - return db; - } - } -} diff --git a/osu-database-reader/Properties/AssemblyInfo.cs b/osu-database-reader/Properties/AssemblyInfo.cs index 719fce5..61befcb 100644 --- a/osu-database-reader/Properties/AssemblyInfo.cs +++ b/osu-database-reader/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/osu-database-reader/Structs.cs b/osu-database-reader/Structs.cs deleted file mode 100644 index 709c156..0000000 --- a/osu-database-reader/Structs.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace osu_database_reader -{ - public struct TimingPoint - { - public double Time, MsPerQuarter; - public bool NotInherited; - } - - public struct Collection - { - public string Name; - public List Md5Hashes; - } -} diff --git a/osu-database-reader/TextFiles/BeatmapFile.cs b/osu-database-reader/TextFiles/BeatmapFile.cs new file mode 100644 index 0000000..a4573a6 --- /dev/null +++ b/osu-database-reader/TextFiles/BeatmapFile.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using osu_database_reader.Components.Beatmaps; +using osu_database_reader.Components.HitObjects; +using osu_database_reader.IO; + +namespace osu_database_reader.TextFiles +{ + public class BeatmapFile + { + public int FileFormatVersion; + + public Dictionary SectionGeneral; + public Dictionary SectionEditor; + public Dictionary SectionMetadata; + public Dictionary SectionDifficulty; + public Dictionary SectionColours; + + public List TimingPoints = new List(); + public List HitObjects = new List(); + + //making some stuff easier to access + public string Artist => SectionMetadata.GetValueOrNull("Artist"); + public string ArtistUnicode => SectionMetadata.GetValueOrNull("ArtistUnicode"); + public string Creator => SectionMetadata.GetValueOrNull("Creator"); + public string Title => SectionMetadata.GetValueOrNull("Title"); + public string TitleUnicode => SectionMetadata.GetValueOrNull("TitleUnicode"); + public string Version => SectionMetadata.GetValueOrNull("Version"); + public string Source => SectionMetadata.GetValueOrNull("Source"); + public string[] Tags => SectionMetadata.GetValueOrNull("Tags")?.Split(' '); + + public float ApproachRate => float.Parse(SectionDifficulty.GetValueOrNull("ApproachRate"), Constants.NumberFormat); + public float HPDrainRate => float.Parse(SectionDifficulty.GetValueOrNull("HPDrainRate"), Constants.NumberFormat); + public float CircleSize => float.Parse(SectionDifficulty.GetValueOrNull("CircleSize"), Constants.NumberFormat); + public float OverallDifficulty => float.Parse(SectionDifficulty.GetValueOrNull("OverallDifficulty"), Constants.NumberFormat); + public float SliderMultiplier => float.Parse(SectionDifficulty.GetValueOrNull("SliderMultiplier"), Constants.NumberFormat); + public float SliderTickRate => float.Parse(SectionDifficulty.GetValueOrNull("SliderTickRate"), Constants.NumberFormat); + + public static BeatmapFile Read(string path) + { + var file = new BeatmapFile(); + + using (var r = new CustomStreamReader(path)) { + if (!int.TryParse(r.ReadLine()?.Replace("osu file format v", string.Empty), out file.FileFormatVersion)) + throw new Exception("Not a valid beatmap"); //very simple check, could be better + + BeatmapSection bs; + while ((bs = r.ReadUntilSectionStart()) != BeatmapSection._EndOfFile) { + switch (bs) { + case BeatmapSection.General: + file.SectionGeneral = r.ReadBasicSection(); + break; + case BeatmapSection.Editor: + file.SectionEditor = r.ReadBasicSection(); + break; + case BeatmapSection.Metadata: + file.SectionMetadata = r.ReadBasicSection(false); + break; + case BeatmapSection.Difficulty: + file.SectionDifficulty = r.ReadBasicSection(false); + break; + case BeatmapSection.Events: + //TODO + r.SkipSection(); + break; + case BeatmapSection.TimingPoints: + file.TimingPoints.AddRange(r.ReadTimingPoints()); + break; + case BeatmapSection.Colours: + file.SectionColours = r.ReadBasicSection(true, true); + break; + case BeatmapSection.HitObjects: + file.HitObjects.AddRange(r.ReadHitObjects()); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + return file; + } + } +} diff --git a/osu-database-reader/osu-database-reader.csproj b/osu-database-reader/osu-database-reader.csproj index 2166c2f..a9531bd 100644 --- a/osu-database-reader/osu-database-reader.csproj +++ b/osu-database-reader/osu-database-reader.csproj @@ -40,18 +40,30 @@ - - - - - - - + + + + + + + + + + + - - - + + + + + + + + + + +