2019-02-12 15:01:25 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2019-01-24 16:43:03 +08:00
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
2022-06-17 15:37:17 +08:00
#nullable disable
2018-06-06 15:20:17 +08:00
using System ;
2017-02-20 00:41:51 +08:00
using System.Collections.Generic ;
2017-11-16 19:06:32 +08:00
using System.Linq ;
2021-11-06 07:19:48 +08:00
using System.Threading ;
2021-11-08 13:43:46 +08:00
using JetBrains.Annotations ;
2019-12-09 16:34:04 +08:00
using osu.Framework.Audio.Track ;
2019-02-21 12:12:37 +08:00
using osu.Framework.Extensions.IEnumerableExtensions ;
2018-05-15 16:38:04 +08:00
using osu.Game.Beatmaps ;
2021-09-30 16:00:15 +08:00
using osu.Game.Beatmaps.ControlPoints ;
using osu.Game.Beatmaps.Timing ;
2019-02-12 15:01:25 +08:00
using osu.Game.Rulesets.Difficulty.Preprocessing ;
using osu.Game.Rulesets.Difficulty.Skills ;
2018-05-15 16:38:04 +08:00
using osu.Game.Rulesets.Mods ;
2019-02-21 12:12:37 +08:00
using osu.Game.Rulesets.Objects ;
2022-02-17 20:14:49 +08:00
using osu.Game.Utils ;
2018-04-13 17:19:50 +08:00
2018-05-15 16:38:04 +08:00
namespace osu.Game.Rulesets.Difficulty
2017-02-20 00:41:51 +08:00
{
2019-02-21 12:12:37 +08:00
public abstract class DifficultyCalculator
2017-02-20 00:41:51 +08:00
{
2021-10-01 18:57:45 +08:00
/// <summary>
/// The beatmap for which difficulty will be calculated.
/// </summary>
protected IBeatmap Beatmap { get ; private set ; }
2019-02-21 12:12:37 +08:00
2021-09-30 16:00:15 +08:00
private Mod [ ] playableMods ;
private double clockRate ;
2021-11-15 17:23:03 +08:00
private readonly IRulesetInfo ruleset ;
2021-11-15 17:19:23 +08:00
private readonly IWorkingBeatmap beatmap ;
2021-10-01 18:57:45 +08:00
2022-07-21 01:05:18 +08:00
/// <summary>
/// A yymmdd version which is used to discern when reprocessing is required.
/// </summary>
public virtual int Version = > 0 ;
2021-11-15 17:23:03 +08:00
protected DifficultyCalculator ( IRulesetInfo ruleset , IWorkingBeatmap beatmap )
2017-02-20 00:41:51 +08:00
{
2019-02-21 12:12:37 +08:00
this . ruleset = ruleset ;
this . beatmap = beatmap ;
}
2021-11-08 13:43:46 +08:00
/// <summary>
/// Calculates the difficulty of the beatmap with no mods applied.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A structure describing the difficulty of the beatmap.</returns>
public DifficultyAttributes Calculate ( CancellationToken cancellationToken = default )
= > Calculate ( Array . Empty < Mod > ( ) , cancellationToken ) ;
2019-02-21 12:12:37 +08:00
/// <summary>
/// Calculates the difficulty of the beatmap using a specific mod combination.
/// </summary>
/// <param name="mods">The mods that should be applied to the beatmap.</param>
2021-11-06 07:19:48 +08:00
/// <param name="cancellationToken">The cancellation token.</param>
2019-02-21 12:12:37 +08:00
/// <returns>A structure describing the difficulty of the beatmap.</returns>
2021-11-08 13:43:46 +08:00
public DifficultyAttributes Calculate ( [ NotNull ] IEnumerable < Mod > mods , CancellationToken cancellationToken = default )
2019-02-21 12:12:37 +08:00
{
2021-11-06 23:03:53 +08:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
2021-11-06 07:19:48 +08:00
preProcess ( mods , cancellationToken ) ;
2019-03-14 22:39:45 +08:00
2021-10-01 18:57:45 +08:00
var skills = CreateSkills ( Beatmap , playableMods , clockRate ) ;
2019-02-21 12:12:37 +08:00
2021-10-01 18:57:45 +08:00
if ( ! Beatmap . HitObjects . Any ( ) )
return CreateDifficultyAttributes ( Beatmap , playableMods , skills , clockRate ) ;
2021-09-30 16:00:15 +08:00
foreach ( var hitObject in getDifficultyHitObjects ( ) )
{
foreach ( var skill in skills )
2021-11-06 23:03:53 +08:00
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
2022-05-23 04:45:27 +08:00
skill . Process ( hitObject ) ;
2021-11-06 23:03:53 +08:00
}
2021-09-30 16:00:15 +08:00
}
2021-10-01 18:57:45 +08:00
return CreateDifficultyAttributes ( Beatmap , playableMods , skills , clockRate ) ;
2021-09-30 16:00:15 +08:00
}
2021-11-02 16:55:00 +08:00
/// <summary>
2021-11-08 13:43:46 +08:00
/// Calculates the difficulty of the beatmap with no mods applied and returns a set of <see cref="TimedDifficultyAttributes"/> representing the difficulty at every relevant time value in the beatmap.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The set of <see cref="TimedDifficultyAttributes"/>.</returns>
public List < TimedDifficultyAttributes > CalculateTimed ( CancellationToken cancellationToken = default )
= > CalculateTimed ( Array . Empty < Mod > ( ) , cancellationToken ) ;
/// <summary>
/// Calculates the difficulty of the beatmap using a specific mod combination and returns a set of <see cref="TimedDifficultyAttributes"/> representing the difficulty at every relevant time value in the beatmap.
2021-11-02 16:55:00 +08:00
/// </summary>
/// <param name="mods">The mods that should be applied to the beatmap.</param>
2021-11-06 07:19:48 +08:00
/// <param name="cancellationToken">The cancellation token.</param>
2021-11-02 16:55:00 +08:00
/// <returns>The set of <see cref="TimedDifficultyAttributes"/>.</returns>
2021-11-08 13:43:46 +08:00
public List < TimedDifficultyAttributes > CalculateTimed ( [ NotNull ] IEnumerable < Mod > mods , CancellationToken cancellationToken = default )
2021-09-30 16:00:15 +08:00
{
2021-11-06 23:03:53 +08:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
2021-11-06 07:19:48 +08:00
preProcess ( mods , cancellationToken ) ;
2021-09-30 16:00:15 +08:00
2021-10-05 15:59:54 +08:00
var attribs = new List < TimedDifficultyAttributes > ( ) ;
2021-10-01 18:57:45 +08:00
if ( ! Beatmap . HitObjects . Any ( ) )
2021-10-05 15:59:54 +08:00
return attribs ;
2021-09-30 16:00:15 +08:00
2021-10-01 18:57:45 +08:00
var skills = CreateSkills ( Beatmap , playableMods , clockRate ) ;
var progressiveBeatmap = new ProgressiveCalculationBeatmap ( Beatmap ) ;
2024-04-11 23:11:54 +08:00
var difficultyObjects = getDifficultyHitObjects ( ) . ToArray ( ) ;
2021-09-30 16:00:15 +08:00
2024-04-11 23:11:54 +08:00
foreach ( var obj in difficultyObjects )
2021-09-30 16:00:15 +08:00
{
2024-04-11 22:40:40 +08:00
// Implementations expect the progressive beatmap to only contain top-level objects from the original beatmap.
// At the same time, we also need to consider the possibility DHOs may not be generated for any given object,
// so we'll add all remaining objects up to the current point in time to the progressive beatmap.
for ( int i = progressiveBeatmap . HitObjects . Count ; i < Beatmap . HitObjects . Count ; i + + )
{
2024-04-11 23:11:54 +08:00
if ( obj ! = difficultyObjects [ ^ 1 ] & & Beatmap . HitObjects [ i ] . StartTime > obj . BaseObject . StartTime )
2024-04-11 22:40:40 +08:00
break ;
progressiveBeatmap . HitObjects . Add ( Beatmap . HitObjects [ i ] ) ;
}
2019-02-21 12:12:37 +08:00
2021-09-30 16:00:15 +08:00
foreach ( var skill in skills )
2021-11-06 23:03:53 +08:00
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
2024-04-11 23:11:54 +08:00
skill . Process ( obj ) ;
2021-11-06 23:03:53 +08:00
}
2021-09-30 16:00:15 +08:00
2024-04-11 23:11:54 +08:00
attribs . Add ( new TimedDifficultyAttributes ( obj . EndTime * clockRate , CreateDifficultyAttributes ( progressiveBeatmap , playableMods , skills , clockRate ) ) ) ;
2021-09-30 16:00:15 +08:00
}
2021-10-05 15:59:54 +08:00
return attribs ;
2019-02-21 12:12:37 +08:00
}
/// <summary>
/// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap.
/// </summary>
2022-02-22 17:12:55 +08:00
/// <remarks>
2022-03-31 14:09:06 +08:00
/// This can only be used to compute difficulties for legacy mod combinations.
2022-02-22 17:12:55 +08:00
/// </remarks>
2019-02-21 12:12:37 +08:00
/// <returns>A collection of structures describing the difficulty of the beatmap for each mod combination.</returns>
2022-03-31 14:09:06 +08:00
public IEnumerable < DifficultyAttributes > CalculateAllLegacyCombinations ( CancellationToken cancellationToken = default )
2019-02-21 12:12:37 +08:00
{
2022-02-17 20:14:49 +08:00
var rulesetInstance = ruleset . CreateInstance ( ) ;
2019-02-21 12:12:37 +08:00
foreach ( var combination in CreateDifficultyAdjustmentModCombinations ( ) )
{
2023-06-02 16:37:43 +08:00
Mod classicMod = rulesetInstance . CreateMod < ModClassic > ( ) ;
2022-02-17 20:14:49 +08:00
var finalCombination = ModUtils . FlattenMod ( combination ) ;
if ( classicMod ! = null )
finalCombination = finalCombination . Append ( classicMod ) ;
yield return Calculate ( finalCombination . ToArray ( ) , cancellationToken ) ;
2019-02-21 12:12:37 +08:00
}
2017-02-20 00:41:51 +08:00
}
2018-04-13 17:19:50 +08:00
2021-09-30 16:00:15 +08:00
/// <summary>
/// Retrieves the <see cref="DifficultyHitObject"/>s to calculate against.
/// </summary>
2021-10-01 18:57:45 +08:00
private IEnumerable < DifficultyHitObject > getDifficultyHitObjects ( ) = > SortObjects ( CreateDifficultyHitObjects ( Beatmap , clockRate ) ) ;
2018-06-14 14:32:07 +08:00
2021-09-30 16:00:15 +08:00
/// <summary>
/// Performs required tasks before every calculation.
/// </summary>
/// <param name="mods">The original list of <see cref="Mod"/>s.</param>
2021-11-07 20:38:00 +08:00
/// <param name="cancellationToken">The cancellation token.</param>
2021-11-08 13:43:46 +08:00
private void preProcess ( [ NotNull ] IEnumerable < Mod > mods , CancellationToken cancellationToken = default )
2021-09-30 16:00:15 +08:00
{
playableMods = mods . Select ( m = > m . DeepClone ( ) ) . ToArray ( ) ;
2021-10-06 20:26:30 +08:00
2021-12-07 12:33:41 +08:00
// Only pass through the cancellation token if it's non-default.
// This allows for the default timeout to be applied for playable beatmap construction.
Beatmap = cancellationToken = = default
? beatmap . GetPlayableBeatmap ( ruleset , playableMods )
: beatmap . GetPlayableBeatmap ( ruleset , playableMods , cancellationToken ) ;
2019-02-12 15:01:25 +08:00
2021-09-30 16:00:15 +08:00
var track = new TrackVirtual ( 10000 ) ;
2021-10-06 20:26:30 +08:00
playableMods . OfType < IApplicableToTrack > ( ) . ForEach ( m = > m . ApplyToTrack ( track ) ) ;
2021-09-30 16:00:15 +08:00
clockRate = track . Rate ;
2017-11-16 19:06:32 +08:00
}
2018-04-13 17:19:50 +08:00
2020-10-09 20:43:46 +08:00
/// <summary>
/// Sorts a given set of <see cref="DifficultyHitObject"/>s.
/// </summary>
/// <param name="input">The <see cref="DifficultyHitObject"/>s to sort.</param>
/// <returns>The sorted <see cref="DifficultyHitObject"/>s.</returns>
protected virtual IEnumerable < DifficultyHitObject > SortObjects ( IEnumerable < DifficultyHitObject > input )
= > input . OrderBy ( h = > h . BaseObject . StartTime ) ;
2018-06-06 15:20:17 +08:00
/// <summary>
2021-10-01 18:57:45 +08:00
/// Creates all <see cref="Mod"/> combinations which adjust the <see cref="Beatmaps.Beatmap"/> difficulty.
2018-06-06 15:20:17 +08:00
/// </summary>
public Mod [ ] CreateDifficultyAdjustmentModCombinations ( )
{
2020-10-14 18:31:31 +08:00
return createDifficultyAdjustmentModCombinations ( DifficultyAdjustmentMods , Array . Empty < Mod > ( ) ) . ToArray ( ) ;
2018-06-06 15:20:17 +08:00
2020-10-14 18:31:31 +08:00
static IEnumerable < Mod > createDifficultyAdjustmentModCombinations ( ReadOnlyMemory < Mod > remainingMods , IEnumerable < Mod > currentSet , int currentSetCount = 0 )
2018-06-06 15:20:17 +08:00
{
2020-10-14 18:31:31 +08:00
// Return the current set.
2018-07-17 13:35:09 +08:00
switch ( currentSetCount )
{
case 0 :
// Initial-case: Empty current set
2018-11-30 16:35:13 +08:00
yield return new ModNoMod ( ) ;
2019-02-28 12:31:40 +08:00
2018-07-17 13:35:09 +08:00
break ;
2019-04-01 11:44:46 +08:00
2018-07-17 13:35:09 +08:00
case 1 :
yield return currentSet . Single ( ) ;
2019-02-28 12:31:40 +08:00
2018-07-17 13:35:09 +08:00
break ;
2019-04-01 11:44:46 +08:00
2018-07-17 15:33:08 +08:00
default :
yield return new MultiMod ( currentSet . ToArray ( ) ) ;
2019-02-28 12:31:40 +08:00
2018-07-17 15:33:08 +08:00
break ;
2018-07-17 13:35:09 +08:00
}
2018-06-06 15:20:17 +08:00
2020-10-14 18:31:31 +08:00
// Apply the rest of the remaining mods recursively.
for ( int i = 0 ; i < remainingMods . Length ; i + + )
2018-06-06 15:20:17 +08:00
{
2021-10-27 12:04:41 +08:00
( var nextSet , int nextCount ) = flatten ( remainingMods . Span [ i ] ) ;
2020-10-14 18:03:11 +08:00
2020-10-14 18:53:37 +08:00
// Check if any mods in the next set are incompatible with any of the current set.
if ( currentSet . SelectMany ( m = > m . IncompatibleMods ) . Any ( c = > nextSet . Any ( c . IsInstanceOfType ) ) )
2018-06-06 15:20:17 +08:00
continue ;
2020-10-14 18:53:37 +08:00
// Check if any mods in the next set are the same type as the current set. Mods of the exact same type are not incompatible with themselves.
if ( currentSet . Any ( c = > nextSet . Any ( n = > c . GetType ( ) = = n . GetType ( ) ) ) )
2018-06-06 15:20:17 +08:00
continue ;
2020-10-14 18:53:37 +08:00
// If all's good, attach the next set to the current set and recurse further.
foreach ( var combo in createDifficultyAdjustmentModCombinations ( remainingMods . Slice ( i + 1 ) , currentSet . Concat ( nextSet ) , currentSetCount + nextCount ) )
2018-06-06 15:20:17 +08:00
yield return combo ;
}
}
2020-10-14 18:03:11 +08:00
2020-10-14 18:31:31 +08:00
// Flattens a mod hierarchy (through MultiMod) as an IEnumerable<Mod>
static ( IEnumerable < Mod > set , int count ) flatten ( Mod mod )
2020-10-14 18:03:11 +08:00
{
2020-10-14 18:31:31 +08:00
if ( ! ( mod is MultiMod multi ) )
return ( mod . Yield ( ) , 1 ) ;
IEnumerable < Mod > set = Enumerable . Empty < Mod > ( ) ;
int count = 0 ;
2020-10-14 18:03:11 +08:00
2020-10-14 18:31:31 +08:00
foreach ( var nested in multi . Mods )
{
2021-10-27 12:04:41 +08:00
( var nestedSet , int nestedCount ) = flatten ( nested ) ;
2020-10-14 18:31:31 +08:00
set = set . Concat ( nestedSet ) ;
count + = nestedCount ;
2020-10-14 18:03:11 +08:00
}
2020-10-14 18:31:31 +08:00
return ( set , count ) ;
2020-10-14 18:03:11 +08:00
}
2018-06-06 15:20:17 +08:00
}
/// <summary>
2021-10-01 18:57:45 +08:00
/// Retrieves all <see cref="Mod"/>s which adjust the <see cref="Beatmaps.Beatmap"/> difficulty.
2018-06-06 15:20:17 +08:00
/// </summary>
protected virtual Mod [ ] DifficultyAdjustmentMods = > Array . Empty < Mod > ( ) ;
2018-06-14 14:32:07 +08:00
/// <summary>
2019-02-19 16:36:33 +08:00
/// Creates <see cref="DifficultyAttributes"/> to describe beatmap's calculated difficulty.
2019-02-12 15:01:25 +08:00
/// </summary>
2021-10-01 18:57:45 +08:00
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty was calculated.
/// This may differ from <see cref="Beatmap"/> in the case of timed calculation.</param>
2019-02-19 16:52:59 +08:00
/// <param name="mods">The <see cref="Mod"/>s that difficulty was calculated with.</param>
/// <param name="skills">The skills which processed the beatmap.</param>
2019-02-19 13:29:23 +08:00
/// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
2019-02-19 16:36:33 +08:00
protected abstract DifficultyAttributes CreateDifficultyAttributes ( IBeatmap beatmap , Mod [ ] mods , Skill [ ] skills , double clockRate ) ;
2019-02-12 15:01:25 +08:00
/// <summary>
/// Enumerates <see cref="DifficultyHitObject"/>s to be processed from <see cref="HitObject"/>s in the <see cref="IBeatmap"/>.
2018-06-14 14:32:07 +08:00
/// </summary>
2019-02-12 15:01:25 +08:00
/// <param name="beatmap">The <see cref="IBeatmap"/> providing the <see cref="HitObject"/>s to enumerate.</param>
2019-02-19 13:29:23 +08:00
/// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
2019-02-12 15:01:25 +08:00
/// <returns>The enumerated <see cref="DifficultyHitObject"/>s.</returns>
2019-02-19 13:29:23 +08:00
protected abstract IEnumerable < DifficultyHitObject > CreateDifficultyHitObjects ( IBeatmap beatmap , double clockRate ) ;
2019-02-12 15:01:25 +08:00
/// <summary>
2019-02-19 16:52:59 +08:00
/// Creates the <see cref="Skill"/>s to calculate the difficulty of an <see cref="IBeatmap"/>.
2019-02-12 15:01:25 +08:00
/// </summary>
2021-10-01 18:57:45 +08:00
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty will be calculated.
/// This may differ from <see cref="Beatmap"/> in the case of timed calculation.</param>
2021-02-06 12:06:16 +08:00
/// <param name="mods">Mods to calculate difficulty with.</param>
2021-06-03 14:09:37 +08:00
/// <param name="clockRate">Clockrate to calculate difficulty with.</param>
2019-02-12 15:01:25 +08:00
/// <returns>The <see cref="Skill"/>s.</returns>
2021-06-03 14:09:37 +08:00
protected abstract Skill [ ] CreateSkills ( IBeatmap beatmap , Mod [ ] mods , double clockRate ) ;
2021-09-30 16:00:15 +08:00
2021-10-01 19:56:03 +08:00
/// <summary>
/// Used to calculate timed difficulty attributes, where only a subset of hitobjects should be visible at any point in time.
/// </summary>
2021-09-30 16:00:15 +08:00
private class ProgressiveCalculationBeatmap : IBeatmap
{
private readonly IBeatmap baseBeatmap ;
public ProgressiveCalculationBeatmap ( IBeatmap baseBeatmap )
{
this . baseBeatmap = baseBeatmap ;
}
2021-10-05 14:10:56 +08:00
public readonly List < HitObject > HitObjects = new List < HitObject > ( ) ;
IReadOnlyList < HitObject > IBeatmap . HitObjects = > HitObjects ;
#region Delegated IBeatmap implementation
2021-09-30 16:00:15 +08:00
public BeatmapInfo BeatmapInfo
{
get = > baseBeatmap . BeatmapInfo ;
set = > baseBeatmap . BeatmapInfo = value ;
}
public ControlPointInfo ControlPointInfo
{
get = > baseBeatmap . ControlPointInfo ;
set = > baseBeatmap . ControlPointInfo = value ;
}
2021-10-05 14:10:56 +08:00
public BeatmapMetadata Metadata = > baseBeatmap . Metadata ;
2021-10-02 11:34:29 +08:00
public BeatmapDifficulty Difficulty
{
get = > baseBeatmap . Difficulty ;
set = > baseBeatmap . Difficulty = value ;
}
2021-09-30 16:00:15 +08:00
public List < BreakPeriod > Breaks = > baseBeatmap . Breaks ;
2024-04-29 22:26:44 +08:00
public List < string > UnhandledEventLines = > baseBeatmap . UnhandledEventLines ;
2021-09-30 16:00:15 +08:00
public double TotalBreakTime = > baseBeatmap . TotalBreakTime ;
public IEnumerable < BeatmapStatistic > GetStatistics ( ) = > baseBeatmap . GetStatistics ( ) ;
public double GetMostCommonBeatLength ( ) = > baseBeatmap . GetMostCommonBeatLength ( ) ;
public IBeatmap Clone ( ) = > new ProgressiveCalculationBeatmap ( baseBeatmap . Clone ( ) ) ;
2021-10-05 14:10:56 +08:00
2024-06-12 18:41:07 +08:00
public double AudioLeadIn
{
get = > baseBeatmap . AudioLeadIn ;
set = > baseBeatmap . AudioLeadIn = value ;
}
2024-06-12 18:50:41 +08:00
public float StackLeniency
{
get = > baseBeatmap . StackLeniency ;
set = > baseBeatmap . StackLeniency = value ;
}
2024-06-12 19:12:30 +08:00
public bool SpecialStyle
{
get = > baseBeatmap . SpecialStyle ;
set = > baseBeatmap . SpecialStyle = value ;
}
2024-06-12 19:15:41 +08:00
public bool LetterboxInBreaks
{
get = > baseBeatmap . LetterboxInBreaks ;
set = > baseBeatmap . LetterboxInBreaks = value ;
}
2024-06-12 19:23:53 +08:00
public bool WidescreenStoryboard
{
get = > baseBeatmap . WidescreenStoryboard ;
set = > baseBeatmap . WidescreenStoryboard = value ;
}
2024-06-12 19:28:41 +08:00
public bool EpilepsyWarning
{
get = > baseBeatmap . EpilepsyWarning ;
set = > baseBeatmap . EpilepsyWarning = value ;
}
2024-06-12 19:32:23 +08:00
public bool SamplesMatchPlaybackRate
{
get = > baseBeatmap . SamplesMatchPlaybackRate ;
set = > baseBeatmap . SamplesMatchPlaybackRate = value ;
}
2024-06-12 19:36:27 +08:00
public double DistanceSpacing
{
get = > baseBeatmap . DistanceSpacing ;
set = > baseBeatmap . DistanceSpacing = value ;
}
2024-06-12 19:44:36 +08:00
public int GridSize
{
get = > baseBeatmap . GridSize ;
set = > baseBeatmap . GridSize = value ;
}
2024-06-12 19:54:31 +08:00
public double TimelineZoom
{
get = > baseBeatmap . TimelineZoom ;
set = > baseBeatmap . TimelineZoom = value ;
}
2021-10-05 14:10:56 +08:00
#endregion
2021-09-30 16:00:15 +08:00
}
2017-02-20 00:41:51 +08:00
}
}