2021-10-11 14:25:00 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System ;
2022-07-13 18:13:18 +08:00
using System.Diagnostics ;
2021-11-22 14:30:11 +08:00
using System.Linq ;
2021-10-11 14:25:00 +08:00
using JetBrains.Annotations ;
using Newtonsoft.Json ;
2022-05-05 15:17:37 +08:00
using osu.Game.Beatmaps.ControlPoints ;
2022-10-11 09:45:33 +08:00
using osu.Game.Collections ;
2021-10-11 14:25:00 +08:00
using osu.Game.Database ;
2021-11-19 18:07:21 +08:00
using osu.Game.Models ;
2021-11-19 18:24:07 +08:00
using osu.Game.Online.API.Requests.Responses ;
2022-03-20 10:01:32 +08:00
using osu.Game.Overlays.BeatmapSet.Scores ;
2021-10-11 14:25:00 +08:00
using osu.Game.Rulesets ;
2022-05-05 15:17:37 +08:00
using osu.Game.Rulesets.Edit ;
2022-01-12 14:09:56 +08:00
using osu.Game.Scoring ;
2021-10-11 14:25:00 +08:00
using Realms ;
2021-11-19 18:07:21 +08:00
namespace osu.Game.Beatmaps
2021-10-11 14:25:00 +08:00
{
/// <summary>
2022-07-28 14:41:28 +08:00
/// A realm model containing metadata for a single beatmap difficulty.
/// This should generally include anything which is required to be filtered on at song select, or anything pertaining to storage of beatmaps in the client.
2021-10-11 14:25:00 +08:00
/// </summary>
2022-07-28 14:41:28 +08:00
/// <remarks>
/// There are some legacy fields in this model which are not persisted to realm. These are isolated in a code region within the class and should eventually be migrated to `Beatmap`.
/// </remarks>
2021-10-11 14:25:00 +08:00
[Serializable]
[MapTo("Beatmap")]
2021-11-19 18:07:21 +08:00
public class BeatmapInfo : RealmObject , IHasGuidPrimaryKey , IBeatmapInfo , IEquatable < BeatmapInfo >
2021-10-11 14:25:00 +08:00
{
[PrimaryKey]
2022-01-18 21:51:45 +08:00
public Guid ID { get ; set ; }
2021-10-11 14:25:00 +08:00
public string DifficultyName { get ; set ; } = string . Empty ;
2022-01-18 21:51:45 +08:00
public RulesetInfo Ruleset { get ; set ; } = null ! ;
2021-10-11 14:25:00 +08:00
2022-01-18 21:51:45 +08:00
public BeatmapDifficulty Difficulty { get ; set ; } = null ! ;
2021-10-11 14:25:00 +08:00
2022-01-18 21:51:45 +08:00
public BeatmapMetadata Metadata { get ; set ; } = null ! ;
2022-01-10 18:23:43 +08:00
2022-01-20 16:09:31 +08:00
[JsonIgnore]
2022-01-12 17:05:25 +08:00
[Backlink(nameof(ScoreInfo.BeatmapInfo))]
2022-01-12 14:09:56 +08:00
public IQueryable < ScoreInfo > Scores { get ; } = null ! ;
2022-03-01 15:22:51 +08:00
public BeatmapUserSettings UserSettings { get ; set ; } = null ! ;
2022-01-18 21:51:45 +08:00
public BeatmapInfo ( RulesetInfo ? ruleset = null , BeatmapDifficulty ? difficulty = null , BeatmapMetadata ? metadata = null )
2022-01-10 18:23:43 +08:00
{
2022-01-18 21:51:45 +08:00
ID = Guid . NewGuid ( ) ;
Ruleset = ruleset ? ? new RulesetInfo
2022-01-17 13:40:00 +08:00
{
OnlineID = 0 ,
ShortName = @"osu" ,
Name = @"null placeholder ruleset"
} ;
2022-01-18 21:51:45 +08:00
Difficulty = difficulty ? ? new BeatmapDifficulty ( ) ;
Metadata = metadata ? ? new BeatmapMetadata ( ) ;
2022-03-01 15:22:51 +08:00
UserSettings = new BeatmapUserSettings ( ) ;
2022-01-18 21:51:45 +08:00
}
[UsedImplicitly]
private BeatmapInfo ( )
{
2022-01-10 18:23:43 +08:00
}
2021-10-11 14:25:00 +08:00
2021-11-19 18:07:21 +08:00
public BeatmapSetInfo ? BeatmapSet { get ; set ; }
2021-10-11 14:25:00 +08:00
2021-11-22 15:45:55 +08:00
[Ignored]
2021-12-21 15:09:32 +08:00
public RealmNamedFileUsage ? File = > BeatmapSet ? . Files . FirstOrDefault ( f = > f . File . Hash = = Hash ) ;
2021-11-22 14:30:11 +08:00
2022-01-18 22:25:30 +08:00
[Ignored]
2021-11-24 17:42:47 +08:00
public BeatmapOnlineStatus Status
2021-10-11 14:25:00 +08:00
{
2021-11-24 17:42:47 +08:00
get = > ( BeatmapOnlineStatus ) StatusInt ;
2021-10-11 14:25:00 +08:00
set = > StatusInt = ( int ) value ;
}
[MapTo(nameof(Status))]
2021-11-24 17:48:12 +08:00
public int StatusInt { get ; set ; } = ( int ) BeatmapOnlineStatus . None ;
2021-10-11 14:25:00 +08:00
2021-10-18 14:35:51 +08:00
[Indexed]
public int OnlineID { get ; set ; } = - 1 ;
2021-10-11 14:25:00 +08:00
public double Length { get ; set ; }
public double BPM { get ; set ; }
public string Hash { get ; set ; } = string . Empty ;
2022-07-21 16:39:07 +08:00
/// <summary>
/// Defaults to -1 (meaning not-yet-calculated).
/// Will likely be superseded with a better storage considering ruleset/mods.
/// </summary>
public double StarRating { get ; set ; } = - 1 ;
2021-10-11 14:25:00 +08:00
2022-06-08 17:01:54 +08:00
[Indexed]
2021-10-11 14:25:00 +08:00
public string MD5Hash { get ; set ; } = string . Empty ;
2022-06-20 17:59:08 +08:00
public string OnlineMD5Hash { get ; set ; } = string . Empty ;
2022-08-01 23:21:28 +08:00
/// <summary>
/// The last time of a local modification (via the editor).
/// </summary>
2022-08-16 15:01:19 +08:00
public DateTimeOffset ? LastLocalUpdate { get ; set ; }
2022-08-01 23:21:28 +08:00
/// <summary>
/// The last time online metadata was applied to this beatmap.
/// </summary>
2022-06-20 17:59:08 +08:00
public DateTimeOffset ? LastOnlineUpdate { get ; set ; }
/// <summary>
/// Whether this beatmap matches the online version, based on fetched online metadata.
/// Will return <c>true</c> if no online metadata is available.
/// </summary>
public bool MatchesOnlineVersion = > LastOnlineUpdate = = null | | MD5Hash = = OnlineMD5Hash ;
2021-10-11 14:25:00 +08:00
[JsonIgnore]
public bool Hidden { get ; set ; }
2023-12-19 17:20:02 +08:00
public int EndTimeObjectCount { get ; set ; } = - 1 ;
2023-12-13 16:33:24 +08:00
2023-12-19 17:20:02 +08:00
public int TotalObjectCount { get ; set ; } = - 1 ;
2023-12-13 16:33:24 +08:00
2022-07-25 17:51:19 +08:00
/// <summary>
/// Reset any fetched online linking information (and history).
/// </summary>
public void ResetOnlineInfo ( )
{
OnlineID = - 1 ;
LastOnlineUpdate = null ;
OnlineMD5Hash = string . Empty ;
2022-08-01 22:57:46 +08:00
if ( Status ! = BeatmapOnlineStatus . LocallyModified )
Status = BeatmapOnlineStatus . None ;
2022-07-25 17:51:19 +08:00
}
2021-10-11 14:25:00 +08:00
#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 ; }
2022-01-28 04:41:30 +08:00
public bool WidescreenStoryboard { get ; set ; } = true ;
2021-10-11 14:25:00 +08:00
public bool EpilepsyWarning { get ; set ; }
2022-01-28 04:41:30 +08:00
public bool SamplesMatchPlaybackRate { get ; set ; } = true ;
2021-10-11 14:25:00 +08:00
2022-07-13 15:36:43 +08:00
/// <summary>
/// The time at which this beatmap was last played by the local user.
/// </summary>
public DateTimeOffset ? LastPlayed { get ; set ; }
2022-05-05 15:17:37 +08:00
/// <summary>
/// The ratio of distance travelled per time unit.
/// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see <see cref="DifficultyControlPoint.SliderVelocity"/>).
/// </summary>
/// <remarks>
/// The most common method of understanding is that at a default value of 1.0, the time-to-distance ratio will match the slider velocity of the beatmap
/// at the current point in time. Increasing this value will make hit objects more spaced apart when compared to the cursor movement required to track a slider.
///
/// This is only a hint property, used by the editor in <see cref="IDistanceSnapProvider"/> implementations. It does not directly affect the beatmap or gameplay.
/// </remarks>
2022-04-29 13:02:07 +08:00
public double DistanceSpacing { get ; set ; } = 1.0 ;
2021-10-11 14:25:00 +08:00
2023-04-20 13:35:12 +08:00
public int BeatDivisor { get ; set ; } = 4 ;
2021-10-11 14:25:00 +08:00
public int GridSize { get ; set ; }
2022-01-25 16:31:05 +08:00
public double TimelineZoom { get ; set ; } = 1.0 ;
2021-10-11 14:25:00 +08:00
2023-06-06 14:11:31 +08:00
/// <summary>
/// The time in milliseconds when last exiting the editor with this beatmap loaded.
/// </summary>
public double? EditorTimestamp { get ; set ; }
2023-06-01 01:06:41 +08:00
2021-12-14 20:00:19 +08:00
[Ignored]
2021-11-19 18:24:07 +08:00
public CountdownType Countdown { get ; set ; } = CountdownType . Normal ;
/// <summary>
/// The number of beats to move the countdown backwards (compared to its default location).
/// </summary>
public int CountdownOffset { get ; set ; }
2021-10-11 14:25:00 +08:00
#endregion
2021-11-19 18:07:21 +08:00
public bool Equals ( BeatmapInfo ? other )
2021-11-12 17:48:34 +08:00
{
if ( ReferenceEquals ( this , other ) ) return true ;
if ( other = = null ) return false ;
return ID = = other . ID ;
}
2021-11-19 18:07:21 +08:00
public bool Equals ( IBeatmapInfo ? other ) = > other is BeatmapInfo b & & Equals ( b ) ;
2021-11-16 11:25:37 +08:00
2021-11-19 18:07:21 +08:00
public bool AudioEquals ( BeatmapInfo ? other ) = > other ! = null
2021-11-19 18:24:07 +08:00
& & BeatmapSet ! = null
& & other . BeatmapSet ! = null
2022-07-13 18:13:18 +08:00
& & compareFiles ( this , other , m = > m . AudioFile ) ;
2021-10-11 14:25:00 +08:00
2021-11-19 18:07:21 +08:00
public bool BackgroundEquals ( BeatmapInfo ? other ) = > other ! = null
2021-11-19 18:24:07 +08:00
& & BeatmapSet ! = null
& & other . BeatmapSet ! = null
2022-07-13 18:13:18 +08:00
& & compareFiles ( this , other , m = > m . BackgroundFile ) ;
private static bool compareFiles ( BeatmapInfo x , BeatmapInfo y , Func < IBeatmapMetadataInfo , string > getFilename )
{
Debug . Assert ( x . BeatmapSet ! = null ) ;
Debug . Assert ( y . BeatmapSet ! = null ) ;
2022-08-10 14:48:38 +08:00
string? fileHashX = x . BeatmapSet . GetFile ( getFilename ( x . Metadata ) ) ? . File . Hash ;
string? fileHashY = y . BeatmapSet . GetFile ( getFilename ( y . Metadata ) ) ? . File . Hash ;
2022-07-13 18:13:18 +08:00
return fileHashX = = fileHashY ;
}
2021-10-11 14:25:00 +08:00
2022-10-11 14:50:02 +08:00
/// <summary>
/// When updating a beatmap, its hashes will change. Collections currently track beatmaps by hash, so they need to be updated.
/// This method will handle updating
/// </summary>
/// <param name="realm">A realm instance in an active write transaction.</param>
/// <param name="previousMD5Hash">The previous MD5 hash of the beatmap before update.</param>
public void TransferCollectionReferences ( Realm realm , string previousMD5Hash )
2022-10-11 09:45:33 +08:00
{
2022-10-11 14:50:02 +08:00
var collections = realm . All < BeatmapCollection > ( ) . AsEnumerable ( ) . Where ( c = > c . BeatmapMD5Hashes . Contains ( previousMD5Hash ) ) ;
2022-10-11 09:45:33 +08:00
foreach ( var c in collections )
{
2022-10-11 14:50:02 +08:00
c . BeatmapMD5Hashes . Remove ( previousMD5Hash ) ;
2022-10-11 09:45:33 +08:00
c . BeatmapMD5Hashes . Add ( MD5Hash ) ;
}
}
2023-07-04 13:51:09 +08:00
/// <summary>
/// Local scores are retained separate from a beatmap's lifetime, matched via <see cref="ScoreInfo.BeatmapHash"/>.
/// Therefore we need to detach / reattach scores when a beatmap is edited or imported.
/// </summary>
/// <param name="realm">A realm instance in an active write transaction.</param>
public void UpdateLocalScores ( Realm realm )
{
// first disassociate any scores which are already attached and no longer valid.
foreach ( var score in Scores )
score . BeatmapInfo = null ;
// then attach any scores which match the new hash.
foreach ( var score in realm . All < ScoreInfo > ( ) . Where ( s = > s . BeatmapHash = = Hash ) )
score . BeatmapInfo = this ;
}
2021-10-11 14:25:00 +08:00
IBeatmapMetadataInfo IBeatmapInfo . Metadata = > Metadata ;
IBeatmapSetInfo ? IBeatmapInfo . BeatmapSet = > BeatmapSet ;
IRulesetInfo IBeatmapInfo . Ruleset = > Ruleset ;
IBeatmapDifficultyInfo IBeatmapInfo . Difficulty = > Difficulty ;
2021-11-19 18:24:07 +08:00
#region Compatibility properties
[Ignored]
public string? Path = > File ? . Filename ;
[Ignored]
public APIBeatmap ? OnlineInfo { get ; set ; }
2022-03-20 10:01:32 +08:00
/// <summary>
/// The maximum achievable combo on this beatmap, populated for online info purposes only.
2022-03-20 21:30:28 +08:00
/// Todo: This should never be used nor exist, but is still relied on in <see cref="ScoresContainer.Scores"/> since <see cref="IBeatmapInfo"/> can't be used yet. For now this is obsoleted until it is removed.
2022-03-20 10:01:32 +08:00
/// </summary>
2021-11-19 18:24:07 +08:00
[Ignored]
2022-03-20 21:30:28 +08:00
[Obsolete("Use ScoreManager.GetMaximumAchievableComboAsync instead.")]
2021-11-19 18:24:07 +08:00
public int? MaxCombo { get ; set ; }
[Ignored]
public int [ ] Bookmarks { get ; set ; } = Array . Empty < int > ( ) ;
public int BeatmapVersion ;
2022-01-14 12:08:20 +08:00
public BeatmapInfo Clone ( ) = > ( BeatmapInfo ) this . Detach ( ) . MemberwiseClone ( ) ;
2021-11-19 18:24:07 +08:00
2022-01-10 11:36:11 +08:00
public override string ToString ( ) = > this . GetDisplayTitle ( ) ;
2022-01-07 13:17:22 +08:00
2021-11-19 18:24:07 +08:00
#endregion
2021-10-11 14:25:00 +08:00
}
}