diff --git a/osu.Game/Database/IHasRealmFiles.cs b/osu.Game/Database/IHasRealmFiles.cs new file mode 100644 index 0000000000..2adfe73d1e --- /dev/null +++ b/osu.Game/Database/IHasRealmFiles.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Models; + +namespace osu.Game.Database +{ + /// + /// A model that contains a list of files it is responsible for. + /// + public interface IHasRealmFiles + { + IList Files { get; } + + string Hash { get; set; } + } +} diff --git a/osu.Game/Database/INamedFile.cs b/osu.Game/Database/INamedFile.cs new file mode 100644 index 0000000000..9c94aed38c --- /dev/null +++ b/osu.Game/Database/INamedFile.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Models; + +namespace osu.Game.Database +{ + /// + /// Represent a join model which gives a filename and scope to a . + /// + public interface INamedFile + { + public string Filename { get; set; } + + public RealmFile File { get; set; } + } +} diff --git a/osu.Game/Models/RealmBeatmap.cs b/osu.Game/Models/RealmBeatmap.cs new file mode 100644 index 0000000000..09f8dafeb6 --- /dev/null +++ b/osu.Game/Models/RealmBeatmap.cs @@ -0,0 +1,122 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using JetBrains.Annotations; +using Newtonsoft.Json; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Rulesets; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + /// + /// A single beatmap difficulty. + /// + [ExcludeFromDynamicCompile] + [Serializable] + [MapTo("Beatmap")] + public class RealmBeatmap : RealmObject, IHasGuidPrimaryKey, IBeatmapInfo + { + [PrimaryKey] + public Guid ID { get; set; } = Guid.NewGuid(); + + public string DifficultyName { get; set; } = string.Empty; + + public RealmRuleset Ruleset { get; set; } = null!; + + public RealmBeatmapDifficulty Difficulty { get; set; } = null!; + + public RealmBeatmapMetadata Metadata { get; set; } = null!; + + public RealmBeatmapSet? BeatmapSet { get; set; } + + public BeatmapSetOnlineStatus Status + { + get => (BeatmapSetOnlineStatus)StatusInt; + set => StatusInt = (int)value; + } + + [MapTo(nameof(Status))] + public int StatusInt { get; set; } + + public int? OnlineID { get; set; } + + public double Length { get; set; } + + public double BPM { get; set; } + + public string Hash { get; set; } = string.Empty; + + public double StarRating { get; set; } + + public string MD5Hash { get; set; } = string.Empty; + + [JsonIgnore] + public bool Hidden { get; set; } + + public RealmBeatmap(RealmRuleset ruleset, RealmBeatmapDifficulty difficulty, RealmBeatmapMetadata metadata) + { + Ruleset = ruleset; + Difficulty = difficulty; + Metadata = metadata; + } + + [UsedImplicitly] + private RealmBeatmap() + { + } + + #region Properties we may not want persisted (but also maybe no harm?) + + public double AudioLeadIn { get; set; } + + public float StackLeniency { get; set; } = 0.7f; + + public bool SpecialStyle { get; set; } + + public bool LetterboxInBreaks { get; set; } + + public bool WidescreenStoryboard { get; set; } + + public bool EpilepsyWarning { get; set; } + + public bool SamplesMatchPlaybackRate { get; set; } + + public double DistanceSpacing { get; set; } + + public int BeatDivisor { get; set; } + + public int GridSize { get; set; } + + public double TimelineZoom { get; set; } + + #endregion + + /// + /// Returns a shallow-clone of this . + /// + public RealmBeatmap Clone() => (RealmBeatmap)MemberwiseClone(); + + public bool AudioEquals(RealmBeatmap? other) => other != null + && BeatmapSet != null + && other.BeatmapSet != null + && BeatmapSet.Hash == other.BeatmapSet.Hash + && Metadata.AudioFile == other.Metadata.AudioFile; + + public bool BackgroundEquals(RealmBeatmap? other) => other != null + && BeatmapSet != null + && other.BeatmapSet != null + && BeatmapSet.Hash == other.BeatmapSet.Hash + && Metadata.BackgroundFile == other.Metadata.BackgroundFile; + + IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata; + IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet; + IRulesetInfo IBeatmapInfo.Ruleset => Ruleset; + IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => Difficulty; + } +} diff --git a/osu.Game/Models/RealmBeatmapDifficulty.cs b/osu.Game/Models/RealmBeatmapDifficulty.cs new file mode 100644 index 0000000000..44bfdda491 --- /dev/null +++ b/osu.Game/Models/RealmBeatmapDifficulty.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [MapTo("BeatmapDifficulty")] + public class RealmBeatmapDifficulty : EmbeddedObject, IBeatmapDifficultyInfo + { + public float DrainRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; + public float CircleSize { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; + public float OverallDifficulty { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; + public float ApproachRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; + + public double SliderMultiplier { get; set; } = 1; + public double SliderTickRate { get; set; } = 1; + + /// + /// Returns a shallow-clone of this . + /// + public RealmBeatmapDifficulty Clone() + { + var diff = new RealmBeatmapDifficulty(); + CopyTo(diff); + return diff; + } + + public void CopyTo(RealmBeatmapDifficulty difficulty) + { + difficulty.ApproachRate = ApproachRate; + difficulty.DrainRate = DrainRate; + difficulty.CircleSize = CircleSize; + difficulty.OverallDifficulty = OverallDifficulty; + + difficulty.SliderMultiplier = SliderMultiplier; + difficulty.SliderTickRate = SliderTickRate; + } + } +} diff --git a/osu.Game/Models/RealmBeatmapMetadata.cs b/osu.Game/Models/RealmBeatmapMetadata.cs new file mode 100644 index 0000000000..00dd120791 --- /dev/null +++ b/osu.Game/Models/RealmBeatmapMetadata.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using Newtonsoft.Json; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [ExcludeFromDynamicCompile] + [Serializable] + [MapTo("BeatmapMetadata")] + public class RealmBeatmapMetadata : RealmObject, IBeatmapMetadataInfo + { + public string Title { get; set; } = string.Empty; + + [JsonProperty("title_unicode")] + public string TitleUnicode { get; set; } = string.Empty; + + public string Artist { get; set; } = string.Empty; + + [JsonProperty("artist_unicode")] + public string ArtistUnicode { get; set; } = string.Empty; + + public string Author { get; set; } = string.Empty; // eventually should be linked to a persisted User. = string.Empty; + + public string Source { get; set; } = string.Empty; + + [JsonProperty(@"tags")] + public string Tags { get; set; } = string.Empty; + + /// + /// The time in milliseconds to begin playing the track for preview purposes. + /// If -1, the track should begin playing at 40% of its length. + /// + public int PreviewTime { get; set; } + + public string AudioFile { get; set; } = string.Empty; + public string BackgroundFile { get; set; } = string.Empty; + } +} diff --git a/osu.Game/Models/RealmBeatmapSet.cs b/osu.Game/Models/RealmBeatmapSet.cs new file mode 100644 index 0000000000..314ca4494b --- /dev/null +++ b/osu.Game/Models/RealmBeatmapSet.cs @@ -0,0 +1,78 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [ExcludeFromDynamicCompile] + [MapTo("BeatmapSet")] + public class RealmBeatmapSet : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable, IBeatmapSetInfo + { + [PrimaryKey] + public Guid ID { get; set; } = Guid.NewGuid(); + + public int? OnlineID { get; set; } + + public DateTimeOffset DateAdded { get; set; } + + public IBeatmapMetadataInfo? Metadata => Beatmaps.FirstOrDefault()?.Metadata; + + public IList Beatmaps { get; } = null!; + + public IList Files { get; } = null!; + + public bool DeletePending { get; set; } + + public string Hash { get; set; } = string.Empty; + + /// + /// Whether deleting this beatmap set should be prohibited (due to it being a system requirement to be present). + /// + public bool Protected { get; set; } + + public double MaxStarDifficulty => Beatmaps.Max(b => b.StarRating); + + public double MaxLength => Beatmaps.Max(b => b.Length); + + public double MaxBPM => Beatmaps.Max(b => b.BPM); + + /// + /// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null. + /// The path returned is relative to the user file storage. + /// + /// The name of the file to get the storage path of. + public string? GetPathForFile(string filename) => Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.StoragePath; + + public override string ToString() => Metadata?.ToString() ?? base.ToString(); + + public bool Equals(RealmBeatmapSet? other) + { + if (other == null) + return false; + + if (IsManaged && other.IsManaged) + return ID == other.ID; + + if (OnlineID.HasValue && other.OnlineID.HasValue) + return OnlineID == other.OnlineID; + + if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash)) + return Hash == other.Hash; + + return ReferenceEquals(this, other); + } + + IEnumerable IBeatmapSetInfo.Beatmaps => Beatmaps; + + IEnumerable IBeatmapSetInfo.Files => Files; + } +} diff --git a/osu.Game/Models/RealmFile.cs b/osu.Game/Models/RealmFile.cs new file mode 100644 index 0000000000..6836d79d2d --- /dev/null +++ b/osu.Game/Models/RealmFile.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using osu.Game.IO; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [MapTo("File")] + public class RealmFile : RealmObject, IFileInfo + { + [PrimaryKey] + public string Hash { get; set; } = string.Empty; + + public string StoragePath => Path.Combine(Hash.Remove(1), Hash.Remove(2), Hash); + } +} diff --git a/osu.Game/Models/RealmNamedFileUsage.cs b/osu.Game/Models/RealmNamedFileUsage.cs new file mode 100644 index 0000000000..59b446112d --- /dev/null +++ b/osu.Game/Models/RealmNamedFileUsage.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Game.Database; +using osu.Game.IO; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + public class RealmNamedFileUsage : EmbeddedObject, INamedFile, INamedFileUsage + { + public RealmFile File { get; set; } = null!; + + public string Filename { get; set; } = null!; + + public RealmNamedFileUsage(RealmFile file, string filename) + { + File = file; + Filename = filename; + } + + [UsedImplicitly] + private RealmNamedFileUsage() + { + } + + IFileInfo INamedFileUsage.File => File; + } +} diff --git a/osu.Game/Models/RealmRuleset.cs b/osu.Game/Models/RealmRuleset.cs new file mode 100644 index 0000000000..0dcd701ed2 --- /dev/null +++ b/osu.Game/Models/RealmRuleset.cs @@ -0,0 +1,63 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using JetBrains.Annotations; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [ExcludeFromDynamicCompile] + [MapTo("Ruleset")] + public class RealmRuleset : RealmObject, IEquatable, IRulesetInfo + { + [PrimaryKey] + public string ShortName { get; set; } = string.Empty; + + public int? OnlineID { get; set; } + + public string Name { get; set; } = string.Empty; + + public string InstantiationInfo { get; set; } = string.Empty; + + public RealmRuleset(string shortName, string name, string instantiationInfo, int? onlineID = null) + { + ShortName = shortName; + Name = name; + InstantiationInfo = instantiationInfo; + OnlineID = onlineID; + } + + [UsedImplicitly] + private RealmRuleset() + { + } + + public RealmRuleset(int? onlineID, string name, string shortName, bool available) + { + OnlineID = onlineID; + Name = name; + ShortName = shortName; + Available = available; + } + + public bool Available { get; set; } + + public bool Equals(RealmRuleset? other) => other != null && OnlineID == other.OnlineID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; + + public override string ToString() => Name; + + public RealmRuleset Clone() => new RealmRuleset + { + OnlineID = OnlineID, + Name = Name, + ShortName = ShortName, + InstantiationInfo = InstantiationInfo, + Available = Available + }; + } +}