2019-01-24 08:43:03 +00: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.
2018-04-13 09:19:50 +00:00
2017-08-13 17:54:07 +00:00
using System ;
2021-09-09 07:34:49 +00:00
using System.Collections.Concurrent ;
2017-08-13 15:41:13 +00:00
using System.Collections.Generic ;
2017-09-07 16:25:33 +00:00
using System.Linq ;
2022-08-29 05:01:04 +00:00
using osu.Framework.Extensions ;
using osu.Framework.Extensions.EnumExtensions ;
2017-08-14 13:16:22 +00:00
using osu.Framework.Graphics ;
2019-03-27 10:29:27 +00:00
using osu.Framework.Graphics.Sprites ;
2017-08-18 22:00:40 +00:00
using osu.Framework.Input.Bindings ;
2019-09-04 11:28:21 +00:00
using osu.Framework.IO.Stores ;
2022-08-29 05:01:04 +00:00
using osu.Framework.Localisation ;
2017-03-10 02:59:08 +00:00
using osu.Game.Beatmaps ;
2018-04-13 13:41:35 +00:00
using osu.Game.Beatmaps.Legacy ;
2018-06-11 04:17:08 +00:00
using osu.Game.Configuration ;
2022-08-29 05:01:04 +00:00
using osu.Game.Extensions ;
using osu.Game.Overlays.Settings ;
2018-06-11 04:17:08 +00:00
using osu.Game.Rulesets.Configuration ;
2018-05-15 08:38:04 +00:00
using osu.Game.Rulesets.Difficulty ;
2022-08-29 05:01:04 +00:00
using osu.Game.Rulesets.Edit ;
using osu.Game.Rulesets.Filter ;
using osu.Game.Rulesets.Mods ;
2022-09-10 03:07:23 +00:00
using osu.Game.Rulesets.Replays.Types ;
2019-12-17 11:08:13 +00:00
using osu.Game.Rulesets.Scoring ;
2022-08-29 05:01:04 +00:00
using osu.Game.Rulesets.UI ;
2018-11-28 07:12:57 +00:00
using osu.Game.Scoring ;
2021-08-22 14:40:17 +00:00
using osu.Game.Screens.Edit.Setup ;
2020-06-15 13:45:18 +00:00
using osu.Game.Screens.Ranking.Statistics ;
2022-08-29 05:01:04 +00:00
using osu.Game.Skinning ;
using osu.Game.Users ;
2018-04-13 09:19:50 +00:00
2017-04-18 07:05:58 +00:00
namespace osu.Game.Rulesets
2016-11-09 10:49:05 +00:00
{
public abstract class Ruleset
{
2022-01-27 06:25:56 +00:00
public RulesetInfo RulesetInfo { get ; }
2018-04-13 09:19:50 +00:00
2021-11-22 07:52:54 +00:00
private static readonly ConcurrentDictionary < string , IMod [ ] > mod_reference_cache = new ConcurrentDictionary < string , IMod [ ] > ( ) ;
2021-09-09 07:34:49 +00:00
2022-08-22 07:10:55 +00:00
/// <summary>
/// Version history:
/// 2022.205.0 FramedReplayInputHandler.CollectPendingInputs renamed to FramedReplayHandler.CollectReplayInputs.
/// 2022.822.0 All strings return values have been converted to LocalisableString to allow for localisation support.
/// </summary>
public const string CURRENT_RULESET_API_VERSION = "2022.822.0" ;
/// <summary>
/// Define the ruleset API version supported by this ruleset.
/// Ruleset implementations should be updated to support the latest version to ensure they can still be loaded.
/// </summary>
/// <remarks>
2022-08-29 05:01:04 +00:00
/// Generally, all ruleset implementations should point this directly to <see cref="CURRENT_RULESET_API_VERSION"/>.
/// This will ensure that each time you compile a new release, it will pull in the most recent version.
2022-08-22 07:10:55 +00:00
/// See https://github.com/ppy/osu/wiki/Breaking-Changes for full details on required ongoing changes.
/// </remarks>
public virtual string RulesetAPIVersionSupported = > string . Empty ;
2021-09-09 07:34:49 +00:00
/// <summary>
2021-09-10 02:09:13 +00:00
/// A queryable source containing all available mods.
/// Call <see cref="IMod.CreateInstance"/> for consumption purposes.
2021-09-09 07:34:49 +00:00
/// </summary>
2021-09-10 02:09:13 +00:00
public IEnumerable < IMod > AllMods
2021-09-09 07:34:49 +00:00
{
2021-09-10 02:09:13 +00:00
get
{
2021-11-22 12:41:09 +00:00
// Is the case for many test usages.
if ( string . IsNullOrEmpty ( ShortName ) )
return CreateAllMods ( ) ;
2021-11-22 07:52:54 +00:00
if ( ! mod_reference_cache . TryGetValue ( ShortName , out var mods ) )
mod_reference_cache [ ShortName ] = mods = CreateAllMods ( ) . Cast < IMod > ( ) . ToArray ( ) ;
2021-09-09 07:34:49 +00:00
2021-09-10 02:09:13 +00:00
return mods ;
}
2021-09-09 07:34:49 +00:00
}
2021-09-10 02:09:13 +00:00
/// <summary>
/// Returns fresh instances of all mods.
/// </summary>
/// <remarks>
/// This comes with considerable allocation overhead. If only accessing for reference purposes (ie. not changing bindables / settings)
/// use <see cref="AllMods"/> instead.
/// </remarks>
2022-12-26 19:36:39 +00:00
public IEnumerable < Mod > CreateAllMods ( ) = > Enum . GetValues < ModType > ( )
2021-09-10 02:09:13 +00:00
// Confine all mods of each mod type into a single IEnumerable<Mod>
. SelectMany ( GetModsFor )
// Filter out all null mods
2022-12-16 09:16:26 +00:00
// This is to handle old rulesets which were doing mods bad. Can be removed at some point we are sure nulls will not appear here.
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
2021-09-10 02:09:13 +00:00
. Where ( mod = > mod ! = null )
// Resolve MultiMods as their .Mods property
. SelectMany ( mod = > ( mod as MultiMod ) ? . Mods ? ? new [ ] { mod } ) ;
2021-09-09 07:34:49 +00:00
/// <summary>
/// Returns a fresh instance of the mod matching the specified acronym.
/// </summary>
/// <param name="acronym">The acronym to query for .</param>
2022-07-10 01:29:17 +00:00
public Mod ? CreateModFromAcronym ( string acronym )
2021-09-09 07:34:49 +00:00
{
2021-09-10 03:05:10 +00:00
return AllMods . FirstOrDefault ( m = > m . Acronym = = acronym ) ? . CreateInstance ( ) ;
2021-09-09 07:34:49 +00:00
}
2021-09-09 07:46:24 +00:00
/// <summary>
/// Returns a fresh instance of the mod matching the specified type.
/// </summary>
2022-07-10 01:29:17 +00:00
public T ? CreateMod < T > ( )
2021-09-09 07:46:24 +00:00
where T : Mod
{
2021-09-10 03:05:10 +00:00
return AllMods . FirstOrDefault ( m = > m is T ) ? . CreateInstance ( ) as T ;
2021-09-09 07:46:24 +00:00
}
2022-06-15 15:26:54 +00:00
/// <summary>
/// Creates an enumerable with mods that are supported by the ruleset for the supplied <paramref name="type"/>.
/// </summary>
/// <remarks>
/// If there are no applicable mods from the given <paramref name="type"/> in this ruleset,
/// then the proper behaviour is to return an empty enumerable.
/// <see langword="null"/> mods should not be present in the returned enumerable.
/// </remarks>
2017-03-02 05:07:28 +00:00
public abstract IEnumerable < Mod > GetModsFor ( ModType type ) ;
2018-04-13 09:19:50 +00:00
2018-04-16 12:14:40 +00:00
/// <summary>
/// Converts mods from legacy enum values. Do not override if you're not a legacy ruleset.
/// </summary>
2020-03-24 03:06:24 +00:00
/// <param name="mods">The legacy enum which will be converted.</param>
/// <returns>An enumerable of constructed <see cref="Mod"/>s.</returns>
public virtual IEnumerable < Mod > ConvertFromLegacyMods ( LegacyMods mods ) = > Array . Empty < Mod > ( ) ;
/// <summary>
/// Converts mods to legacy enum values. Do not override if you're not a legacy ruleset.
/// </summary>
/// <param name="mods">The mods which will be converted.</param>
/// <returns>A single bitwise enumerable value representing (to the best of our ability) the mods.</returns>
public virtual LegacyMods ConvertToLegacyMods ( Mod [ ] mods )
{
var value = LegacyMods . None ;
foreach ( var mod in mods )
{
switch ( mod )
{
2022-06-24 12:25:23 +00:00
case ModNoFail :
2020-03-24 03:06:24 +00:00
value | = LegacyMods . NoFail ;
break ;
2022-06-24 12:25:23 +00:00
case ModEasy :
2020-03-24 03:06:24 +00:00
value | = LegacyMods . Easy ;
break ;
2022-06-24 12:25:23 +00:00
case ModHidden :
2020-03-24 03:06:24 +00:00
value | = LegacyMods . Hidden ;
break ;
2022-06-24 12:25:23 +00:00
case ModHardRock :
2020-03-24 03:06:24 +00:00
value | = LegacyMods . HardRock ;
break ;
2022-06-24 12:25:23 +00:00
case ModPerfect :
2022-06-30 02:52:29 +00:00
value | = LegacyMods . Perfect | LegacyMods . SuddenDeath ;
2020-11-15 14:35:06 +00:00
break ;
2022-06-24 12:25:23 +00:00
case ModSuddenDeath :
2020-03-24 03:06:24 +00:00
value | = LegacyMods . SuddenDeath ;
break ;
2022-06-24 12:25:23 +00:00
case ModNightcore :
2022-06-30 02:52:29 +00:00
value | = LegacyMods . Nightcore | LegacyMods . DoubleTime ;
2020-11-15 14:35:06 +00:00
break ;
2022-06-24 12:25:23 +00:00
case ModDoubleTime :
2020-03-24 03:06:24 +00:00
value | = LegacyMods . DoubleTime ;
break ;
2022-06-24 12:25:23 +00:00
case ModRelax :
2020-03-24 03:06:24 +00:00
value | = LegacyMods . Relax ;
break ;
2022-06-24 12:25:23 +00:00
case ModHalfTime :
2020-03-24 03:06:24 +00:00
value | = LegacyMods . HalfTime ;
break ;
2022-06-24 12:25:23 +00:00
case ModFlashlight :
2020-03-24 03:06:24 +00:00
value | = LegacyMods . Flashlight ;
break ;
2020-11-15 14:35:06 +00:00
2022-06-24 12:25:23 +00:00
case ModCinema :
2022-06-30 02:52:29 +00:00
value | = LegacyMods . Cinema | LegacyMods . Autoplay ;
2020-11-15 14:35:06 +00:00
break ;
2022-06-24 12:25:23 +00:00
case ModAutoplay :
2020-11-15 14:35:06 +00:00
value | = LegacyMods . Autoplay ;
break ;
2023-07-09 14:15:17 +00:00
case ModScoreV2 :
value | = LegacyMods . ScoreV2 ;
break ;
2020-03-24 03:06:24 +00:00
}
}
return value ;
}
2018-04-13 13:41:35 +00:00
2022-07-10 01:29:17 +00:00
public ModAutoplay ? GetAutoplayMod ( ) = > CreateMod < ModAutoplay > ( ) ;
2018-04-13 09:19:50 +00:00
2023-10-30 12:24:01 +00:00
public ModTouchDevice ? GetTouchDeviceMod ( ) = > CreateMod < ModTouchDevice > ( ) ;
2022-09-15 08:36:14 +00:00
/// <summary>
/// Create a transformer which adds lookups specific to a ruleset to skin sources.
/// </summary>
/// <param name="skin">The source skin.</param>
/// <param name="beatmap">The current beatmap.</param>
/// <returns>A skin with a transformer applied, or null if no transformation is provided by this ruleset.</returns>
public virtual ISkin ? CreateSkinTransformer ( ISkin skin , IBeatmap beatmap ) = > null ;
2019-08-26 03:21:49 +00:00
2019-12-18 05:49:09 +00:00
protected Ruleset ( )
2017-08-09 04:04:11 +00:00
{
2019-12-24 07:02:35 +00:00
RulesetInfo = new RulesetInfo
{
Name = Description ,
ShortName = ShortName ,
2021-11-22 05:26:24 +00:00
OnlineID = ( this as ILegacyRuleset ) ? . LegacyID ? ? - 1 ,
2021-05-12 20:42:26 +00:00
InstantiationInfo = GetType ( ) . GetInvariantInstantiationInfo ( ) ,
2020-01-03 11:39:15 +00:00
Available = true ,
2019-12-24 07:02:35 +00:00
} ;
2017-08-09 04:04:11 +00:00
}
2018-04-13 09:19:50 +00:00
2017-04-20 02:36:50 +00:00
/// <summary>
2017-05-19 06:57:32 +00:00
/// Attempt to create a hit renderer for a beatmap
2017-04-20 02:36:50 +00:00
/// </summary>
2017-05-19 06:57:32 +00:00
/// <param name="beatmap">The beatmap to create the hit renderer for.</param>
2019-04-25 08:36:17 +00:00
/// <param name="mods">The <see cref="Mod"/>s to apply.</param>
2017-04-20 02:36:50 +00:00
/// <exception cref="BeatmapInvalidForRulesetException">Unable to successfully load the beatmap to be usable with this ruleset.</exception>
2022-07-10 01:29:17 +00:00
public abstract DrawableRuleset CreateDrawableRulesetWith ( IBeatmap beatmap , IReadOnlyList < Mod > ? mods = null ) ;
2018-04-13 09:19:50 +00:00
2019-12-17 11:08:13 +00:00
/// <summary>
2019-12-24 08:01:17 +00:00
/// Creates a <see cref="ScoreProcessor"/> for this <see cref="Ruleset"/>.
2019-12-17 11:08:13 +00:00
/// </summary>
/// <returns>The score processor.</returns>
2022-03-14 06:51:10 +00:00
public virtual ScoreProcessor CreateScoreProcessor ( ) = > new ScoreProcessor ( this ) ;
2019-12-17 11:08:13 +00:00
2019-12-19 11:03:14 +00:00
/// <summary>
2019-12-24 08:01:17 +00:00
/// Creates a <see cref="HealthProcessor"/> for this <see cref="Ruleset"/>.
2019-12-19 11:03:14 +00:00
/// </summary>
/// <returns>The health processor.</returns>
2019-12-27 07:14:49 +00:00
public virtual HealthProcessor CreateHealthProcessor ( double drainStartTime ) = > new DrainingHealthProcessor ( drainStartTime ) ;
2019-12-19 11:03:14 +00:00
2018-06-29 04:07:00 +00:00
/// <summary>
/// Creates a <see cref="IBeatmapConverter"/> to convert a <see cref="IBeatmap"/> to one that is applicable for this <see cref="Ruleset"/>.
/// </summary>
/// <param name="beatmap">The <see cref="IBeatmap"/> to be converted.</param>
/// <returns>The <see cref="IBeatmapConverter"/>.</returns>
2018-04-19 13:04:12 +00:00
public abstract IBeatmapConverter CreateBeatmapConverter ( IBeatmap beatmap ) ;
2018-06-29 04:07:00 +00:00
/// <summary>
/// Optionally creates a <see cref="IBeatmapProcessor"/> to alter a <see cref="IBeatmap"/> after it has been converted.
/// </summary>
/// <param name="beatmap">The <see cref="IBeatmap"/> to be processed.</param>
/// <returns>The <see cref="IBeatmapProcessor"/>.</returns>
2022-07-10 02:07:09 +00:00
public virtual IBeatmapProcessor ? CreateBeatmapProcessor ( IBeatmap beatmap ) = > null ;
2018-04-19 13:04:12 +00:00
2021-11-15 09:19:23 +00:00
public abstract DifficultyCalculator CreateDifficultyCalculator ( IWorkingBeatmap beatmap ) ;
2018-04-13 09:19:50 +00:00
2020-10-07 08:46:57 +00:00
/// <summary>
/// Optionally creates a <see cref="PerformanceCalculator"/> to generate performance data from the provided score.
/// </summary>
/// <returns>A performance calculator instance for the provided score.</returns>
2022-07-10 01:29:17 +00:00
public virtual PerformanceCalculator ? CreatePerformanceCalculator ( ) = > null ;
2018-04-13 09:19:50 +00:00
2022-07-10 02:09:32 +00:00
public virtual HitObjectComposer ? CreateHitObjectComposer ( ) = > null ;
2018-04-13 09:19:50 +00:00
2022-07-10 01:35:46 +00:00
public virtual IBeatmapVerifier ? CreateBeatmapVerifier ( ) = > null ;
2021-04-07 12:36:43 +00:00
2019-04-02 10:55:24 +00:00
public virtual Drawable CreateIcon ( ) = > new SpriteIcon { Icon = FontAwesome . Solid . QuestionCircle } ;
2018-04-13 09:19:50 +00:00
2019-12-28 13:13:18 +00:00
public virtual IResourceStore < byte [ ] > CreateResourceStore ( ) = > new NamespacedResourceStore < byte [ ] > ( new DllResourceStore ( GetType ( ) . Assembly ) , @"Resources" ) ;
2019-09-04 11:28:21 +00:00
2017-03-09 20:37:03 +00:00
public abstract string Description { get ; }
2018-04-13 09:19:50 +00:00
2022-07-10 01:29:17 +00:00
public virtual RulesetSettingsSubsection ? CreateSettings ( ) = > null ;
2018-04-13 09:19:50 +00:00
2018-06-11 04:17:08 +00:00
/// <summary>
/// Creates the <see cref="IRulesetConfigManager"/> for this <see cref="Ruleset"/>.
/// </summary>
/// <param name="settings">The <see cref="SettingsStore"/> to store the settings.</param>
2022-07-10 02:01:56 +00:00
public virtual IRulesetConfigManager ? CreateConfig ( SettingsStore ? settings ) = > null ;
2018-06-11 04:17:08 +00:00
2017-12-08 09:55:25 +00:00
/// <summary>
/// A unique short name to reference this ruleset in online requests.
/// </summary>
public abstract string ShortName { get ; }
2018-04-13 09:19:50 +00:00
2020-01-03 11:39:15 +00:00
/// <summary>
2021-08-18 00:13:53 +00:00
/// The playing verb to be shown in the <see cref="UserActivity.InGame"/> activities.
2020-01-03 11:39:15 +00:00
/// </summary>
2021-08-14 14:39:12 +00:00
public virtual string PlayingVerb = > "Playing" ;
2020-01-03 11:39:15 +00:00
2017-08-14 11:19:25 +00:00
/// <summary>
/// A list of available variant ids.
/// </summary>
public virtual IEnumerable < int > AvailableVariants = > new [ ] { 0 } ;
2018-04-13 09:19:50 +00:00
2017-08-14 11:19:25 +00:00
/// <summary>
/// Get a list of default keys for the specified variant.
/// </summary>
/// <param name="variant">A variant.</param>
/// <returns>A list of valid <see cref="KeyBinding"/>s.</returns>
2019-11-28 13:41:29 +00:00
public virtual IEnumerable < KeyBinding > GetDefaultKeyBindings ( int variant = 0 ) = > Array . Empty < KeyBinding > ( ) ;
2018-04-13 09:19:50 +00:00
2017-08-23 05:19:14 +00:00
/// <summary>
/// Gets the name for a key binding variant. This is used for display in the settings overlay.
/// </summary>
/// <param name="variant">The variant.</param>
/// <returns>A descriptive name of the variant.</returns>
2022-08-22 05:51:00 +00:00
public virtual LocalisableString GetVariantName ( int variant ) = > string . Empty ;
2018-04-13 09:19:50 +00:00
2022-09-10 03:07:23 +00:00
/// <summary>
/// For rulesets which support legacy (osu-stable) replay conversion, this method will create an empty replay frame
/// for conversion use.
/// </summary>
/// <returns>An empty frame for the current ruleset, or null if unsupported.</returns>
public virtual IConvertibleReplayFrame ? CreateConvertibleReplayFrame ( ) = > null ;
2020-06-19 11:53:43 +00:00
/// <summary>
/// Creates the statistics for a <see cref="ScoreInfo"/> to be displayed in the results screen.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to create the statistics for. The score is guaranteed to have <see cref="ScoreInfo.HitEvents"/> populated.</param>
2020-06-22 09:38:41 +00:00
/// <param name="playableBeatmap">The <see cref="IBeatmap"/>, converted for this <see cref="Ruleset"/> with all relevant <see cref="Mod"/>s applied.</param>
2023-06-01 05:35:14 +00:00
/// <returns>The <see cref="StatisticItem"/>s to display.</returns>
public virtual StatisticItem [ ] CreateStatisticsForScore ( ScoreInfo score , IBeatmap playableBeatmap ) = > Array . Empty < StatisticItem > ( ) ;
2020-10-07 06:34:23 +00:00
/// <summary>
/// Get all valid <see cref="HitResult"/>s for this ruleset.
/// Generally used for results display purposes, where it can't be determined if zero-count means the user has not achieved any or the type is not used by this ruleset.
/// </summary>
/// <returns>
/// All valid <see cref="HitResult"/>s along with a display-friendly name.
/// </returns>
2022-08-14 18:54:02 +00:00
public IEnumerable < ( HitResult result , LocalisableString displayName ) > GetHitResults ( )
2020-10-07 06:34:23 +00:00
{
var validResults = GetValidHitResults ( ) ;
// enumerate over ordered list to guarantee return order is stable.
2021-01-27 21:01:56 +00:00
foreach ( var result in EnumExtensions . GetValuesInOrder < HitResult > ( ) )
2020-10-07 06:34:23 +00:00
{
switch ( result )
{
// hard blocked types, should never be displayed even if the ruleset tells us to.
case HitResult . None :
case HitResult . IgnoreHit :
case HitResult . IgnoreMiss :
// display is handled as a completion count with corresponding "hit" type.
case HitResult . LargeTickMiss :
case HitResult . SmallTickMiss :
continue ;
}
if ( result = = HitResult . Miss | | validResults . Contains ( result ) )
yield return ( result , GetDisplayNameForHitResult ( result ) ) ;
}
}
/// <summary>
/// Get all valid <see cref="HitResult"/>s for this ruleset.
/// Generally used for results display purposes, where it can't be determined if zero-count means the user has not achieved any or the type is not used by this ruleset.
/// </summary>
/// <remarks>
/// <see cref="HitResult.Miss"/> is implicitly included. Special types like <see cref="HitResult.IgnoreHit"/> are ignored even when specified.
/// </remarks>
2021-01-27 21:01:56 +00:00
protected virtual IEnumerable < HitResult > GetValidHitResults ( ) = > EnumExtensions . GetValuesInOrder < HitResult > ( ) ;
2020-10-07 06:34:23 +00:00
/// <summary>
/// Get a display friendly name for the specified result type.
/// </summary>
/// <param name="result">The result type to get the name for.</param>
/// <returns>The display name.</returns>
2022-08-14 18:54:02 +00:00
public virtual LocalisableString GetDisplayNameForHitResult ( HitResult result ) = > result . GetLocalisableDescription ( ) ;
2021-03-02 19:07:11 +00:00
2023-11-17 08:04:02 +00:00
/// <summary>
/// Applies changes to difficulty attributes for presenting to a user a rough estimate of how rate adjust mods affect difficulty.
/// Importantly, this should NOT BE USED FOR ANY CALCULATIONS.
/// It is also not always correct, and arguably is never correct depending on your frame of mind.
/// </summary>
/// <param name="difficulty">>The <see cref="IBeatmapDifficultyInfo"/> that will be adjusted.</param>
/// <param name="rate">The rate adjustment multiplier from mods. For example 1.5 for DT.</param>
/// <returns>The adjusted difficulty attributes.</returns>
public virtual BeatmapDifficulty GetRateAdjustedDisplayDifficulty ( IBeatmapDifficultyInfo difficulty , double rate ) = > new BeatmapDifficulty ( difficulty ) ;
2021-03-02 19:07:11 +00:00
/// <summary>
/// Creates ruleset-specific beatmap filter criteria to be used on the song select screen.
/// </summary>
2022-07-10 01:29:17 +00:00
public virtual IRulesetFilterCriteria ? CreateRulesetFilterCriteria ( ) = > null ;
2021-08-22 14:40:17 +00:00
/// <summary>
/// Can be overridden to add a ruleset-specific section to the editor beatmap setup screen.
/// </summary>
2022-07-10 01:29:17 +00:00
public virtual RulesetSetupSection ? CreateEditorSetupSection ( ) = > null ;
2023-07-21 09:52:19 +00:00
/// <summary>
/// Can be overridden to alter the difficulty section to the editor beatmap setup screen.
/// </summary>
public virtual DifficultySection ? CreateEditorDifficultySection ( ) = > null ;
2016-11-09 10:49:05 +00:00
}
}