2021-10-11 15:25:00 +09: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 19:13:18 +09:00
using System.Diagnostics ;
2021-11-22 15:30:11 +09:00
using System.Linq ;
2021-10-11 15:25:00 +09:00
using JetBrains.Annotations ;
using Newtonsoft.Json ;
2022-05-05 16:17:37 +09:00
using osu.Game.Beatmaps.ControlPoints ;
2022-10-10 21:45:33 -04:00
using osu.Game.Collections ;
2021-10-11 15:25:00 +09:00
using osu.Game.Database ;
2021-11-19 19:07:21 +09:00
using osu.Game.Models ;
2021-11-19 19:24:07 +09:00
using osu.Game.Online.API.Requests.Responses ;
2022-03-20 05:01:32 +03:00
using osu.Game.Overlays.BeatmapSet.Scores ;
2021-10-11 15:25:00 +09:00
using osu.Game.Rulesets ;
2022-05-05 16:17:37 +09:00
using osu.Game.Rulesets.Edit ;
2022-01-12 15:09:56 +09:00
using osu.Game.Scoring ;
2021-10-11 15:25:00 +09:00
using Realms ;
2021-11-19 19:07:21 +09:00
namespace osu.Game.Beatmaps
2021-10-11 15:25:00 +09:00
{
/// <summary>
2022-07-28 15:41:28 +09: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 15:25:00 +09:00
/// </summary>
2022-07-28 15:41:28 +09: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 15:25:00 +09:00
[Serializable]
[MapTo("Beatmap")]
2021-11-19 19:07:21 +09:00
public class BeatmapInfo : RealmObject , IHasGuidPrimaryKey , IBeatmapInfo , IEquatable < BeatmapInfo >
2021-10-11 15:25:00 +09:00
{
[PrimaryKey]
2022-01-18 22:51:45 +09:00
public Guid ID { get ; set ; }
2021-10-11 15:25:00 +09:00
public string DifficultyName { get ; set ; } = string . Empty ;
2022-01-18 22:51:45 +09:00
public RulesetInfo Ruleset { get ; set ; } = null ! ;
2021-10-11 15:25:00 +09:00
2022-01-18 22:51:45 +09:00
public BeatmapDifficulty Difficulty { get ; set ; } = null ! ;
2021-10-11 15:25:00 +09:00
2022-01-18 22:51:45 +09:00
public BeatmapMetadata Metadata { get ; set ; } = null ! ;
2022-01-10 19:23:43 +09:00
2022-01-20 17:09:31 +09:00
[JsonIgnore]
2022-01-12 18:05:25 +09:00
[Backlink(nameof(ScoreInfo.BeatmapInfo))]
2022-01-12 15:09:56 +09:00
public IQueryable < ScoreInfo > Scores { get ; } = null ! ;
2022-03-01 16:22:51 +09:00
public BeatmapUserSettings UserSettings { get ; set ; } = null ! ;
2022-01-18 22:51:45 +09:00
public BeatmapInfo ( RulesetInfo ? ruleset = null , BeatmapDifficulty ? difficulty = null , BeatmapMetadata ? metadata = null )
2022-01-10 19:23:43 +09:00
{
2022-01-18 22:51:45 +09:00
ID = Guid . NewGuid ( ) ;
Ruleset = ruleset ? ? new RulesetInfo
2022-01-17 14:40:00 +09:00
{
OnlineID = 0 ,
ShortName = @"osu" ,
Name = @"null placeholder ruleset"
} ;
2022-01-18 22:51:45 +09:00
Difficulty = difficulty ? ? new BeatmapDifficulty ( ) ;
Metadata = metadata ? ? new BeatmapMetadata ( ) ;
2022-03-01 16:22:51 +09:00
UserSettings = new BeatmapUserSettings ( ) ;
2022-01-18 22:51:45 +09:00
}
[UsedImplicitly]
private BeatmapInfo ( )
{
2022-01-10 19:23:43 +09:00
}
2021-10-11 15:25:00 +09:00
2021-11-19 19:07:21 +09:00
public BeatmapSetInfo ? BeatmapSet { get ; set ; }
2021-10-11 15:25:00 +09:00
2021-11-22 16:45:55 +09:00
[Ignored]
2021-12-21 16:09:32 +09:00
public RealmNamedFileUsage ? File = > BeatmapSet ? . Files . FirstOrDefault ( f = > f . File . Hash = = Hash ) ;
2021-11-22 15:30:11 +09:00
2022-01-18 23:25:30 +09:00
[Ignored]
2021-11-24 18:42:47 +09:00
public BeatmapOnlineStatus Status
2021-10-11 15:25:00 +09:00
{
2021-11-24 18:42:47 +09:00
get = > ( BeatmapOnlineStatus ) StatusInt ;
2021-10-11 15:25:00 +09:00
set = > StatusInt = ( int ) value ;
}
[MapTo(nameof(Status))]
2021-11-24 18:48:12 +09:00
public int StatusInt { get ; set ; } = ( int ) BeatmapOnlineStatus . None ;
2021-10-11 15:25:00 +09:00
2021-10-18 15:35:51 +09:00
[Indexed]
public int OnlineID { get ; set ; } = - 1 ;
2021-10-11 15:25:00 +09:00
public double Length { get ; set ; }
public double BPM { get ; set ; }
public string Hash { get ; set ; } = string . Empty ;
2022-07-21 17:39:07 +09: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 15:25:00 +09:00
2022-06-08 18:01:54 +09:00
[Indexed]
2021-10-11 15:25:00 +09:00
public string MD5Hash { get ; set ; } = string . Empty ;
2022-06-20 18:59:08 +09:00
public string OnlineMD5Hash { get ; set ; } = string . Empty ;
2022-08-02 00:21:28 +09:00
/// <summary>
/// The last time of a local modification (via the editor).
/// </summary>
2022-08-16 16:01:19 +09:00
public DateTimeOffset ? LastLocalUpdate { get ; set ; }
2022-08-02 00:21:28 +09:00
/// <summary>
/// The last time online metadata was applied to this beatmap.
/// </summary>
2022-06-20 18:59:08 +09: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 15:25:00 +09:00
[JsonIgnore]
public bool Hidden { get ; set ; }
2023-12-19 18:20:02 +09:00
public int EndTimeObjectCount { get ; set ; } = - 1 ;
2023-12-13 17:33:24 +09:00
2023-12-19 18:20:02 +09:00
public int TotalObjectCount { get ; set ; } = - 1 ;
2023-12-13 17:33:24 +09:00
2022-07-25 18:51:19 +09:00
/// <summary>
/// Reset any fetched online linking information (and history).
/// </summary>
public void ResetOnlineInfo ( )
{
OnlineID = - 1 ;
LastOnlineUpdate = null ;
OnlineMD5Hash = string . Empty ;
2022-08-01 23:57:46 +09:00
if ( Status ! = BeatmapOnlineStatus . LocallyModified )
Status = BeatmapOnlineStatus . None ;
2022-07-25 18:51:19 +09:00
}
2021-10-11 15:25:00 +09: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-27 21:41:30 +01:00
public bool WidescreenStoryboard { get ; set ; } = true ;
2021-10-11 15:25:00 +09:00
public bool EpilepsyWarning { get ; set ; }
2022-01-27 21:41:30 +01:00
public bool SamplesMatchPlaybackRate { get ; set ; } = true ;
2021-10-11 15:25:00 +09:00
2022-07-13 16:36:43 +09:00
/// <summary>
/// The time at which this beatmap was last played by the local user.
/// </summary>
public DateTimeOffset ? LastPlayed { get ; set ; }
2022-05-05 16:17:37 +09: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 08:02:07 +03:00
public double DistanceSpacing { get ; set ; } = 1.0 ;
2021-10-11 15:25:00 +09:00
2023-04-20 14:35:12 +09:00
public int BeatDivisor { get ; set ; } = 4 ;
2021-10-11 15:25:00 +09:00
public int GridSize { get ; set ; }
2022-01-25 11:31:05 +03:00
public double TimelineZoom { get ; set ; } = 1.0 ;
2021-10-11 15:25:00 +09:00
2023-06-06 15:11:31 +09:00
/// <summary>
/// The time in milliseconds when last exiting the editor with this beatmap loaded.
/// </summary>
public double? EditorTimestamp { get ; set ; }
2023-05-31 19:06:41 +02:00
2021-12-14 21:00:19 +09:00
[Ignored]
2021-11-19 19:24:07 +09: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 15:25:00 +09:00
#endregion
2021-11-19 19:07:21 +09:00
public bool Equals ( BeatmapInfo ? other )
2021-11-12 18:48:34 +09:00
{
if ( ReferenceEquals ( this , other ) ) return true ;
if ( other = = null ) return false ;
return ID = = other . ID ;
}
2021-11-19 19:07:21 +09:00
public bool Equals ( IBeatmapInfo ? other ) = > other is BeatmapInfo b & & Equals ( b ) ;
2021-11-16 12:25:37 +09:00
2021-11-19 19:07:21 +09:00
public bool AudioEquals ( BeatmapInfo ? other ) = > other ! = null
2021-11-19 19:24:07 +09:00
& & BeatmapSet ! = null
& & other . BeatmapSet ! = null
2022-07-13 19:13:18 +09:00
& & compareFiles ( this , other , m = > m . AudioFile ) ;
2021-10-11 15:25:00 +09:00
2021-11-19 19:07:21 +09:00
public bool BackgroundEquals ( BeatmapInfo ? other ) = > other ! = null
2021-11-19 19:24:07 +09:00
& & BeatmapSet ! = null
& & other . BeatmapSet ! = null
2022-07-13 19:13:18 +09: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 15:48:38 +09:00
string? fileHashX = x . BeatmapSet . GetFile ( getFilename ( x . Metadata ) ) ? . File . Hash ;
string? fileHashY = y . BeatmapSet . GetFile ( getFilename ( y . Metadata ) ) ? . File . Hash ;
2022-07-13 19:13:18 +09:00
return fileHashX = = fileHashY ;
}
2021-10-11 15:25:00 +09:00
2022-10-11 15:50:02 +09: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-10 21:45:33 -04:00
{
2022-10-11 15:50:02 +09:00
var collections = realm . All < BeatmapCollection > ( ) . AsEnumerable ( ) . Where ( c = > c . BeatmapMD5Hashes . Contains ( previousMD5Hash ) ) ;
2022-10-10 21:45:33 -04:00
foreach ( var c in collections )
{
2022-10-11 15:50:02 +09:00
c . BeatmapMD5Hashes . Remove ( previousMD5Hash ) ;
2022-10-10 21:45:33 -04:00
c . BeatmapMD5Hashes . Add ( MD5Hash ) ;
}
}
2023-07-04 14:51:09 +09: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 15:25:00 +09:00
IBeatmapMetadataInfo IBeatmapInfo . Metadata = > Metadata ;
IBeatmapSetInfo ? IBeatmapInfo . BeatmapSet = > BeatmapSet ;
IRulesetInfo IBeatmapInfo . Ruleset = > Ruleset ;
IBeatmapDifficultyInfo IBeatmapInfo . Difficulty = > Difficulty ;
2021-11-19 19:24:07 +09:00
#region Compatibility properties
[Ignored]
public string? Path = > File ? . Filename ;
[Ignored]
public APIBeatmap ? OnlineInfo { get ; set ; }
2022-03-20 05:01:32 +03:00
/// <summary>
/// The maximum achievable combo on this beatmap, populated for online info purposes only.
2022-03-20 16:30:28 +03: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 05:01:32 +03:00
/// </summary>
2021-11-19 19:24:07 +09:00
[Ignored]
2022-03-20 16:30:28 +03:00
[Obsolete("Use ScoreManager.GetMaximumAchievableComboAsync instead.")]
2021-11-19 19:24:07 +09:00
public int? MaxCombo { get ; set ; }
[Ignored]
public int [ ] Bookmarks { get ; set ; } = Array . Empty < int > ( ) ;
public int BeatmapVersion ;
2022-01-14 13:08:20 +09:00
public BeatmapInfo Clone ( ) = > ( BeatmapInfo ) this . Detach ( ) . MemberwiseClone ( ) ;
2021-11-19 19:24:07 +09:00
2022-01-10 12:36:11 +09:00
public override string ToString ( ) = > this . GetDisplayTitle ( ) ;
2022-01-07 14:17:22 +09:00
2021-11-19 19:24:07 +09:00
#endregion
2021-10-11 15:25:00 +09:00
}
}