mirror of
https://github.com/osukey/osukey.git
synced 2025-08-04 23:24:04 +09:00
Merge branch 'master' into osu-distance-spacing
This commit is contained in:
51
osu.Game/Rulesets/AssemblyRulesetStore.cs
Normal file
51
osu.Game/Rulesets/AssemblyRulesetStore.cs
Normal file
@ -0,0 +1,51 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Platform;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Rulesets
|
||||
{
|
||||
/// <summary>
|
||||
/// A ruleset store that populates from loaded assemblies (and optionally, assemblies in a storage).
|
||||
/// </summary>
|
||||
public class AssemblyRulesetStore : RulesetStore
|
||||
{
|
||||
public override IEnumerable<RulesetInfo> AvailableRulesets => availableRulesets;
|
||||
|
||||
private readonly List<RulesetInfo> availableRulesets = new List<RulesetInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Create an assembly ruleset store that populates from loaded assemblies and an external location.
|
||||
/// </summary>
|
||||
/// <param name="path">An path containing ruleset DLLs.</param>
|
||||
public AssemblyRulesetStore(string path)
|
||||
: this(new NativeStorage(path))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an assembly ruleset store that populates from loaded assemblies and an optional storage source.
|
||||
/// </summary>
|
||||
/// <param name="storage">An optional storage containing ruleset DLLs.</param>
|
||||
public AssemblyRulesetStore(Storage? storage = null)
|
||||
: base(storage)
|
||||
|
||||
{
|
||||
List<Ruleset> instances = LoadedAssemblies.Values
|
||||
.Select(r => Activator.CreateInstance(r) as Ruleset)
|
||||
.Where(r => r != null)
|
||||
.Select(r => r.AsNonNull())
|
||||
.ToList();
|
||||
|
||||
// add all legacy rulesets first to ensure they have exclusive choice of primary key.
|
||||
foreach (var r in instances.Where(r => r is ILegacyRuleset))
|
||||
availableRulesets.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID));
|
||||
}
|
||||
}
|
||||
}
|
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
public Mod[] Mods { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The combined star rating of all skill.
|
||||
/// The combined star rating of all skills.
|
||||
/// </summary>
|
||||
[JsonProperty("star_rating", Order = -3)]
|
||||
public double StarRating { get; set; }
|
||||
|
@ -15,6 +15,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Difficulty
|
||||
{
|
||||
@ -119,15 +120,23 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
/// <summary>
|
||||
/// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This can only be used to compute difficulties for legacy mod combinations.
|
||||
/// </remarks>
|
||||
/// <returns>A collection of structures describing the difficulty of the beatmap for each mod combination.</returns>
|
||||
public IEnumerable<DifficultyAttributes> CalculateAll(CancellationToken cancellationToken = default)
|
||||
public IEnumerable<DifficultyAttributes> CalculateAllLegacyCombinations(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var rulesetInstance = ruleset.CreateInstance();
|
||||
|
||||
foreach (var combination in CreateDifficultyAdjustmentModCombinations())
|
||||
{
|
||||
if (combination is MultiMod multi)
|
||||
yield return Calculate(multi.Mods, cancellationToken);
|
||||
else
|
||||
yield return Calculate(combination.Yield(), cancellationToken);
|
||||
Mod classicMod = rulesetInstance.CreateAllMods().SingleOrDefault(m => m is ModClassic);
|
||||
|
||||
var finalCombination = ModUtils.FlattenMod(combination);
|
||||
if (classicMod != null)
|
||||
finalCombination = finalCombination.Append(classicMod);
|
||||
|
||||
yield return Calculate(finalCombination.ToArray(), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,9 +63,8 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
|
||||
// calculate total score
|
||||
ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
|
||||
scoreProcessor.HighestCombo.Value = perfectPlay.MaxCombo;
|
||||
scoreProcessor.Mods.Value = perfectPlay.Mods;
|
||||
perfectPlay.TotalScore = (long)scoreProcessor.GetImmediateScore(ScoringMode.Standardised, perfectPlay.MaxCombo, statistics);
|
||||
perfectPlay.TotalScore = (long)scoreProcessor.ComputeFinalScore(ScoringMode.Standardised, perfectPlay);
|
||||
|
||||
// compute rank achieved
|
||||
// default to SS, then adjust the rank with mods
|
||||
@ -85,7 +84,7 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
).ConfigureAwait(false);
|
||||
|
||||
// ScorePerformanceCache is not used to avoid caching multiple copies of essentially identical perfect performance attributes
|
||||
return difficulty == null ? null : ruleset.CreatePerformanceCalculator(difficulty.Value.Attributes, perfectPlay)?.Calculate();
|
||||
return difficulty == null ? null : ruleset.CreatePerformanceCalculator()?.Calculate(perfectPlay, difficulty.Value.Attributes);
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
|
@ -1,41 +1,31 @@
|
||||
// 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;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Difficulty
|
||||
{
|
||||
public abstract class PerformanceCalculator
|
||||
{
|
||||
protected readonly DifficultyAttributes Attributes;
|
||||
|
||||
protected readonly Ruleset Ruleset;
|
||||
protected readonly ScoreInfo Score;
|
||||
|
||||
protected double TimeRate { get; private set; } = 1;
|
||||
|
||||
protected PerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
|
||||
protected PerformanceCalculator(Ruleset ruleset)
|
||||
{
|
||||
Ruleset = ruleset;
|
||||
Score = score;
|
||||
|
||||
Attributes = attributes ?? throw new ArgumentNullException(nameof(attributes));
|
||||
|
||||
ApplyMods(score.Mods);
|
||||
}
|
||||
|
||||
protected virtual void ApplyMods(Mod[] mods)
|
||||
{
|
||||
var track = new TrackVirtual(10000);
|
||||
mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
TimeRate = track.Rate;
|
||||
}
|
||||
public PerformanceAttributes Calculate(ScoreInfo score, DifficultyAttributes attributes)
|
||||
=> CreatePerformanceAttributes(score, attributes);
|
||||
|
||||
public abstract PerformanceAttributes Calculate();
|
||||
public PerformanceAttributes Calculate(ScoreInfo score, IWorkingBeatmap beatmap)
|
||||
=> Calculate(score, Ruleset.CreateDifficultyCalculator(beatmap).Calculate(score.Mods));
|
||||
|
||||
/// <summary>
|
||||
/// Creates <see cref="PerformanceAttributes"/> to describe a score's performance.
|
||||
/// </summary>
|
||||
/// <param name="score">The score to create the attributes for.</param>
|
||||
/// <param name="attributes">The difficulty attributes for the beatmap relating to the score.</param>
|
||||
protected abstract PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes);
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
private void regenerateAutoplay()
|
||||
{
|
||||
var autoplayMod = drawableRuleset.Mods.OfType<ModAutoplay>().Single();
|
||||
drawableRuleset.SetReplayScore(autoplayMod.CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods));
|
||||
drawableRuleset.SetReplayScore(autoplayMod.CreateScoreFromReplayData(drawableRuleset.Beatmap, drawableRuleset.Mods));
|
||||
}
|
||||
|
||||
private void addHitObject(HitObject hitObject)
|
||||
|
@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
protected readonly OsuScrollContainer Scroll;
|
||||
|
||||
protected readonly FillFlowContainer FillFlow;
|
||||
|
||||
protected override Container<Drawable> Content { get; }
|
||||
|
||||
public ScrollingToolboxGroup(string title, float scrollAreaHeight)
|
||||
@ -20,7 +22,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = scrollAreaHeight,
|
||||
Child = Content = new FillFlowContainer
|
||||
Child = Content = FillFlow = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
|
@ -1,14 +1,22 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public interface ICreateReplay
|
||||
[Obsolete("Use ICreateReplayData instead")] // Can be removed 20220929
|
||||
public interface ICreateReplay : ICreateReplayData
|
||||
{
|
||||
public Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods);
|
||||
|
||||
ModReplayData ICreateReplayData.CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
{
|
||||
var replayScore = CreateReplayScore(beatmap, mods);
|
||||
return new ModReplayData(replayScore.Replay, new ModCreatedUser { Username = replayScore.ScoreInfo.User.Username });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
63
osu.Game/Rulesets/Mods/ICreateReplayData.cs
Normal file
63
osu.Game/Rulesets/Mods/ICreateReplayData.cs
Normal file
@ -0,0 +1,63 @@
|
||||
// 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.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
/// <summary>
|
||||
/// A mod which creates full replay data, which is to be played back in place of a local user playing the game.
|
||||
/// </summary>
|
||||
public interface ICreateReplayData
|
||||
{
|
||||
/// <summary>
|
||||
/// Create replay data.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap to create replay data for.</param>
|
||||
/// <param name="mods">The mods to take into account when creating the replay data.</param>
|
||||
/// <returns>A <see cref="ModReplayData"/> structure, containing the generated replay data.</returns>
|
||||
/// <remarks>
|
||||
/// For callers that want to receive a directly usable <see cref="Score"/> instance,
|
||||
/// the <see cref="ModExtensions.CreateScoreFromReplayData"/> extension method is provided for convenience.
|
||||
/// </remarks>
|
||||
ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data created by a mod that implements <see cref="ICreateReplayData"/>.
|
||||
/// </summary>
|
||||
public class ModReplayData
|
||||
{
|
||||
/// <summary>
|
||||
/// The full replay data.
|
||||
/// </summary>
|
||||
public readonly Replay Replay;
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder user data to show in place of the local user when the associated mod is active.
|
||||
/// </summary>
|
||||
public readonly ModCreatedUser User;
|
||||
|
||||
public ModReplayData(Replay replay, ModCreatedUser user = null)
|
||||
{
|
||||
Replay = replay;
|
||||
User = user ?? new ModCreatedUser();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A user which is associated with a replay that was created by a mod (ie. autoplay or cinema).
|
||||
/// </summary>
|
||||
public class ModCreatedUser : IUser
|
||||
{
|
||||
public int OnlineID => APIUser.SYSTEM_USER_ID;
|
||||
public bool IsBot => true;
|
||||
|
||||
public string Username { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
@ -5,8 +5,19 @@ using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface for <see cref="Mod"/>s that are updated every frame by a <see cref="Playfield"/>.
|
||||
/// </summary>
|
||||
public interface IUpdatableByPlayfield : IApplicableMod
|
||||
{
|
||||
/// <summary>
|
||||
/// Update this <see cref="Mod"/>.
|
||||
/// </summary>
|
||||
/// <param name="playfield">The main <see cref="Playfield"/></param>
|
||||
/// <remarks>
|
||||
/// This method is called once per frame during gameplay by the main <see cref="Playfield"/> only.
|
||||
/// To access nested <see cref="Playfield"/>s, use <see cref="Playfield.NestedPlayfields"/>.
|
||||
/// </remarks>
|
||||
void Update(Playfield playfield);
|
||||
}
|
||||
}
|
||||
|
@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
hashCode.Add(GetType());
|
||||
|
||||
foreach (var setting in Settings)
|
||||
hashCode.Add(ModUtils.GetSettingUnderlyingValue(setting));
|
||||
hashCode.Add(setting.GetUnderlyingSettingValue());
|
||||
|
||||
return hashCode.ToHashCode();
|
||||
}
|
||||
@ -208,13 +208,13 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public bool Equals(IBindable x, IBindable y)
|
||||
{
|
||||
object xValue = x == null ? null : ModUtils.GetSettingUnderlyingValue(x);
|
||||
object yValue = y == null ? null : ModUtils.GetSettingUnderlyingValue(y);
|
||||
object xValue = x?.GetUnderlyingSettingValue();
|
||||
object yValue = y?.GetUnderlyingSettingValue();
|
||||
|
||||
return EqualityComparer<object>.Default.Equals(xValue, yValue);
|
||||
}
|
||||
|
||||
public int GetHashCode(IBindable obj) => ModUtils.GetSettingUnderlyingValue(obj).GetHashCode();
|
||||
public int GetHashCode(IBindable obj) => obj.GetUnderlyingSettingValue().GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
269
osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs
Normal file
269
osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs
Normal file
@ -0,0 +1,269 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Audio;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public class ModAdaptiveSpeed : Mod, IApplicableToRate, IApplicableToDrawableHitObject, IApplicableToBeatmap, IUpdatableByPlayfield
|
||||
{
|
||||
public override string Name => "Adaptive Speed";
|
||||
|
||||
public override string Acronym => "AS";
|
||||
|
||||
public override string Description => "Let track speed adapt to you.";
|
||||
|
||||
public override ModType Type => ModType.Fun;
|
||||
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp) };
|
||||
|
||||
[SettingSource("Initial rate", "The starting speed of the track")]
|
||||
public BindableNumber<double> InitialRate { get; } = new BindableDouble
|
||||
{
|
||||
MinValue = 0.5,
|
||||
MaxValue = 2,
|
||||
Default = 1,
|
||||
Value = 1,
|
||||
Precision = 0.01
|
||||
};
|
||||
|
||||
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
|
||||
public BindableBool AdjustPitch { get; } = new BindableBool
|
||||
{
|
||||
Default = true,
|
||||
Value = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The instantaneous rate of the track.
|
||||
/// Every frame this mod will attempt to smoothly adjust this to meet <see cref="targetRate"/>.
|
||||
/// </summary>
|
||||
public BindableNumber<double> SpeedChange { get; } = new BindableDouble
|
||||
{
|
||||
MinValue = min_allowable_rate,
|
||||
MaxValue = max_allowable_rate,
|
||||
Default = 1,
|
||||
Value = 1
|
||||
};
|
||||
|
||||
// The two constants below denote the maximum allowable range of rates that `SpeedChange` can take.
|
||||
// The range is purposefully wider than the range of values that `InitialRate` allows
|
||||
// in order to give some leeway for change even when extreme initial rates are chosen.
|
||||
private const double min_allowable_rate = 0.4d;
|
||||
private const double max_allowable_rate = 2.5d;
|
||||
|
||||
// The two constants below denote the maximum allowable change in rate caused by a single hit
|
||||
// This prevents sudden jolts caused by a badly-timed hit.
|
||||
private const double min_allowable_rate_change = 0.9d;
|
||||
private const double max_allowable_rate_change = 1.11d;
|
||||
|
||||
// Apply a fixed rate change when missing, allowing the player to catch up when the rate is too fast.
|
||||
private const double rate_change_on_miss = 0.95d;
|
||||
|
||||
private ITrack track;
|
||||
private double targetRate = 1d;
|
||||
|
||||
/// <summary>
|
||||
/// The number of most recent track rates (approximated from how early/late each object was hit relative to the previous object)
|
||||
/// which should be averaged to calculate <see cref="targetRate"/>.
|
||||
/// </summary>
|
||||
private const int recent_rate_count = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the most recent <see cref="recent_rate_count"/> approximated track rates
|
||||
/// which are averaged to calculate the value of <see cref="targetRate"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This list is used as a double-ended queue with fixed capacity
|
||||
/// (items can be enqueued/dequeued at either end of the list).
|
||||
/// When time is elapsing forward, items are dequeued from the start and enqueued onto the end of the list.
|
||||
/// When time is being rewound, items are dequeued from the end and enqueued onto the start of the list.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <para>
|
||||
/// The track rate approximation is calculated as follows:
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Consider a hitobject which ends at 1000ms, and assume that its preceding hitobject ends at 500ms.
|
||||
/// This gives a time difference of 1000 - 500 = 500ms.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Now assume that the user hit this object at 980ms rather than 1000ms.
|
||||
/// When compared to the preceding hitobject, this gives 980 - 500 = 480ms.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// With the above assumptions, the player is rushing / hitting early, which means that the track should speed up to match.
|
||||
/// Therefore, the approximated target rate for this object would be equal to 500 / 480 * <see cref="InitialRate"/>.
|
||||
/// </para>
|
||||
/// </example>
|
||||
private readonly List<double> recentRates = Enumerable.Repeat(1d, recent_rate_count).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// For each given <see cref="HitObject"/> in the map, this dictionary maps the object onto the latest end time of any other object
|
||||
/// that precedes the end time of the given object.
|
||||
/// This can be loosely interpreted as the end time of the preceding hit object in rulesets that do not have overlapping hit objects.
|
||||
/// </summary>
|
||||
private readonly Dictionary<HitObject, double> precedingEndTimes = new Dictionary<HitObject, double>();
|
||||
|
||||
/// <summary>
|
||||
/// For each given <see cref="HitObject"/> in the map, this dictionary maps the object onto the track rate dequeued from
|
||||
/// <see cref="recentRates"/> (i.e. the oldest value in the queue) when the object is hit. If the hit is then reverted,
|
||||
/// the mapped value can be re-introduced to <see cref="recentRates"/> to properly rewind the queue.
|
||||
/// </summary>
|
||||
private readonly Dictionary<HitObject, double> ratesForRewinding = new Dictionary<HitObject, double>();
|
||||
|
||||
public ModAdaptiveSpeed()
|
||||
{
|
||||
InitialRate.BindValueChanged(val =>
|
||||
{
|
||||
SpeedChange.Value = val.NewValue;
|
||||
targetRate = val.NewValue;
|
||||
});
|
||||
AdjustPitch.BindValueChanged(adjustPitchChanged);
|
||||
}
|
||||
|
||||
public void ApplyToTrack(ITrack track)
|
||||
{
|
||||
this.track = track;
|
||||
|
||||
InitialRate.TriggerChange();
|
||||
AdjustPitch.TriggerChange();
|
||||
recentRates.Clear();
|
||||
recentRates.AddRange(Enumerable.Repeat(InitialRate.Value, recent_rate_count));
|
||||
}
|
||||
|
||||
public void ApplyToSample(DrawableSample sample)
|
||||
{
|
||||
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
|
||||
}
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
SpeedChange.Value = Interpolation.DampContinuously(SpeedChange.Value, targetRate, 50, playfield.Clock.ElapsedFrameTime);
|
||||
}
|
||||
|
||||
public double ApplyToRate(double time, double rate = 1) => rate * InitialRate.Value;
|
||||
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
|
||||
{
|
||||
drawable.OnNewResult += (o, result) =>
|
||||
{
|
||||
if (ratesForRewinding.ContainsKey(result.HitObject)) return;
|
||||
if (!shouldProcessResult(result)) return;
|
||||
|
||||
ratesForRewinding.Add(result.HitObject, recentRates[0]);
|
||||
recentRates.RemoveAt(0);
|
||||
|
||||
recentRates.Add(Math.Clamp(getRelativeRateChange(result) * SpeedChange.Value, min_allowable_rate, max_allowable_rate));
|
||||
|
||||
updateTargetRate();
|
||||
};
|
||||
drawable.OnRevertResult += (o, result) =>
|
||||
{
|
||||
if (!ratesForRewinding.ContainsKey(result.HitObject)) return;
|
||||
if (!shouldProcessResult(result)) return;
|
||||
|
||||
recentRates.Insert(0, ratesForRewinding[result.HitObject]);
|
||||
ratesForRewinding.Remove(result.HitObject);
|
||||
|
||||
recentRates.RemoveAt(recentRates.Count - 1);
|
||||
|
||||
updateTargetRate();
|
||||
};
|
||||
}
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
var hitObjects = getAllApplicableHitObjects(beatmap.HitObjects).ToList();
|
||||
var endTimes = hitObjects.Select(x => x.GetEndTime()).OrderBy(x => x).Distinct().ToList();
|
||||
|
||||
foreach (HitObject hitObject in hitObjects)
|
||||
{
|
||||
int index = endTimes.BinarySearch(hitObject.GetEndTime());
|
||||
if (index < 0) index = ~index; // BinarySearch returns the next larger element in bitwise complement if there's no exact match
|
||||
index -= 1;
|
||||
|
||||
if (index >= 0)
|
||||
precedingEndTimes.Add(hitObject, endTimes[index]);
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustPitchChanged(ValueChangedEvent<bool> adjustPitchSetting)
|
||||
{
|
||||
track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange);
|
||||
|
||||
track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange);
|
||||
}
|
||||
|
||||
private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue)
|
||||
=> adjustPitchSettingValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo;
|
||||
|
||||
private IEnumerable<HitObject> getAllApplicableHitObjects(IEnumerable<HitObject> hitObjects)
|
||||
{
|
||||
foreach (var hitObject in hitObjects)
|
||||
{
|
||||
if (!(hitObject.HitWindows is HitWindows.EmptyHitWindows))
|
||||
yield return hitObject;
|
||||
|
||||
foreach (HitObject nested in getAllApplicableHitObjects(hitObject.NestedHitObjects))
|
||||
yield return nested;
|
||||
}
|
||||
}
|
||||
|
||||
private bool shouldProcessResult(JudgementResult result)
|
||||
{
|
||||
if (!result.Type.AffectsAccuracy()) return false;
|
||||
if (!precedingEndTimes.ContainsKey(result.HitObject)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private double getRelativeRateChange(JudgementResult result)
|
||||
{
|
||||
if (!result.IsHit)
|
||||
return rate_change_on_miss;
|
||||
|
||||
double prevEndTime = precedingEndTimes[result.HitObject];
|
||||
return Math.Clamp(
|
||||
(result.HitObject.GetEndTime() - prevEndTime) / (result.TimeAbsolute - prevEndTime),
|
||||
min_allowable_rate_change,
|
||||
max_allowable_rate_change
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update <see cref="targetRate"/> based on the values in <see cref="recentRates"/>.
|
||||
/// </summary>
|
||||
private void updateTargetRate()
|
||||
{
|
||||
// Compare values in recentRates to see how consistent the player's speed is
|
||||
// If the player hits half of the notes too fast and the other half too slow: Abs(consistency) = 0
|
||||
// If the player hits all their notes too fast or too slow: Abs(consistency) = recent_rate_count - 1
|
||||
int consistency = 0;
|
||||
|
||||
for (int i = 1; i < recentRates.Count; i++)
|
||||
{
|
||||
consistency += Math.Sign(recentRates[i] - recentRates[i - 1]);
|
||||
}
|
||||
|
||||
// Scale the rate adjustment based on consistency
|
||||
targetRate = Interpolation.Lerp(targetRate, recentRates.Average(), Math.Abs(consistency) / (recent_rate_count - 1d));
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModAutoplay : Mod, IApplicableFailOverride, ICreateReplay
|
||||
public abstract class ModAutoplay : Mod, IApplicableFailOverride, ICreateReplayData
|
||||
{
|
||||
public override string Name => "Autoplay";
|
||||
public override string Acronym => "AT";
|
||||
@ -26,10 +26,20 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public override bool UserPlayable => false;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) };
|
||||
|
||||
public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;
|
||||
|
||||
[Obsolete("Override CreateReplayData(IBeatmap, IReadOnlyList<Mod>) instead")] // Can be removed 20220929
|
||||
public virtual Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score { Replay = new Replay() };
|
||||
|
||||
public virtual ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
var replayScore = CreateReplayScore(beatmap, mods);
|
||||
#pragma warning restore CS0618
|
||||
|
||||
return new ModReplayData(replayScore.Replay, new ModCreatedUser { Username = replayScore.ScoreInfo.User.Username });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override string Description => "The whole playfield is on a wheel!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override string SettingDescription => $"{SpinSpeed.Value} rpm {Direction.Value.GetDescription().ToLowerInvariant()}";
|
||||
public override string SettingDescription => $"{SpinSpeed.Value:N2} rpm {Direction.Value.GetDescription().ToLowerInvariant()}";
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
|
@ -1,6 +1,8 @@
|
||||
// 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;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -14,8 +16,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public virtual void ApplyToDrawableRuleset(DrawableRuleset<T> drawableRuleset)
|
||||
{
|
||||
drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods));
|
||||
|
||||
// AlwaysPresent required for hitsounds
|
||||
drawableRuleset.AlwaysPresent = true;
|
||||
drawableRuleset.Hide();
|
||||
@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override IconUsage? Icon => OsuIcon.ModCinema;
|
||||
public override string Description => "Watch the video without visual distractions.";
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAutoplay)).ToArray();
|
||||
|
||||
public void ApplyToHUD(HUDOverlay overlay)
|
||||
{
|
||||
overlay.ShowHud.Value = false;
|
||||
|
@ -1,8 +1,6 @@
|
||||
// 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;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Configuration;
|
||||
@ -18,8 +16,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override string Description => "Zoooooooooom...";
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModHalfTime)).ToArray();
|
||||
|
||||
[SettingSource("Speed increase", "The actual increase to apply")]
|
||||
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble
|
||||
{
|
||||
|
31
osu.Game/Rulesets/Mods/ModExtensions.cs
Normal file
31
osu.Game/Rulesets/Mods/ModExtensions.cs
Normal file
@ -0,0 +1,31 @@
|
||||
// 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.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public static class ModExtensions
|
||||
{
|
||||
public static Score CreateScoreFromReplayData(this ICreateReplayData mod, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
{
|
||||
var replayData = mod.CreateReplayData(beatmap, mods);
|
||||
|
||||
return new Score
|
||||
{
|
||||
Replay = replayData.Replay,
|
||||
ScoreInfo =
|
||||
{
|
||||
User = new APIUser
|
||||
{
|
||||
Id = APIUser.SYSTEM_USER_ID,
|
||||
Username = replayData.User.Username,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
// 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;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Configuration;
|
||||
@ -18,8 +16,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override ModType Type => ModType.DifficultyReduction;
|
||||
public override string Description => "Less zoom...";
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModDoubleTime)).ToArray();
|
||||
|
||||
[SettingSource("Speed decrease", "The actual decrease to apply")]
|
||||
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble
|
||||
{
|
||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public double ApplyToRate(double time, double rate) => rate * SpeedChange.Value;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp), typeof(ModAdaptiveSpeed), typeof(ModRateAdjust) };
|
||||
|
||||
public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x";
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
|
||||
public abstract BindableBool AdjustPitch { get; }
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) };
|
||||
|
||||
public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x";
|
||||
|
||||
|
29
osu.Game/Rulesets/Mods/UnknownMod.cs
Normal file
29
osu.Game/Rulesets/Mods/UnknownMod.cs
Normal file
@ -0,0 +1,29 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public class UnknownMod : Mod
|
||||
{
|
||||
/// <summary>
|
||||
/// The acronym of the mod which could not be resolved.
|
||||
/// </summary>
|
||||
public readonly string OriginalAcronym;
|
||||
|
||||
public override string Name => $"Unknown mod ({OriginalAcronym})";
|
||||
public override string Acronym => $"{OriginalAcronym}??";
|
||||
public override string Description => "This mod could not be resolved by the game.";
|
||||
public override double ScoreMultiplier => 0;
|
||||
|
||||
public override bool UserPlayable => false;
|
||||
|
||||
public override ModType Type => ModType.System;
|
||||
|
||||
public UnknownMod(string acronym)
|
||||
{
|
||||
OriginalAcronym = acronym;
|
||||
}
|
||||
|
||||
public override Mod DeepClone() => new UnknownMod(OriginalAcronym);
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
@ -67,6 +68,12 @@ namespace osu.Game.Rulesets.Objects
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Any samples which may be used by this hit object that are non-standard.
|
||||
/// This is used only to preload these samples ahead of time.
|
||||
/// </summary>
|
||||
public virtual IList<HitSampleInfo> AuxiliarySamples => ImmutableList<HitSampleInfo>.Empty;
|
||||
|
||||
public SampleControlPoint SampleControlPoint = SampleControlPoint.DEFAULT;
|
||||
public DifficultyControlPoint DifficultyControlPoint = DifficultyControlPoint.DEFAULT;
|
||||
|
||||
|
@ -500,6 +500,9 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
=> new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered));
|
||||
|
||||
public bool Equals(LegacyHitSampleInfo? other)
|
||||
// The additions to equality checks here are *required* to ensure that pooling works correctly.
|
||||
// Of note, `IsLayered` may cause the usage of `SampleVirtual` instead of an actual sample (in cases playback is not required).
|
||||
// Removing it would cause samples which may actually require playback to potentially source for a `SampleVirtual` sample pool.
|
||||
=> base.Equals(other) && CustomSampleBank == other.CustomSampleBank && IsLayered == other.IsLayered;
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
|
101
osu.Game/Rulesets/RealmRulesetStore.cs
Normal file
101
osu.Game/Rulesets/RealmRulesetStore.cs
Normal file
@ -0,0 +1,101 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Rulesets
|
||||
{
|
||||
public class RealmRulesetStore : RulesetStore
|
||||
{
|
||||
public override IEnumerable<RulesetInfo> AvailableRulesets => availableRulesets;
|
||||
|
||||
private readonly List<RulesetInfo> availableRulesets = new List<RulesetInfo>();
|
||||
|
||||
public RealmRulesetStore(RealmAccess realm, Storage? storage = null)
|
||||
: base(storage)
|
||||
{
|
||||
prepareDetachedRulesets(realm);
|
||||
}
|
||||
|
||||
private void prepareDetachedRulesets(RealmAccess realmAccess)
|
||||
{
|
||||
realmAccess.Write(realm =>
|
||||
{
|
||||
var rulesets = realm.All<RulesetInfo>();
|
||||
|
||||
List<Ruleset> instances = LoadedAssemblies.Values
|
||||
.Select(r => Activator.CreateInstance(r) as Ruleset)
|
||||
.Where(r => r != null)
|
||||
.Select(r => r.AsNonNull())
|
||||
.ToList();
|
||||
|
||||
// add all legacy rulesets first to ensure they have exclusive choice of primary key.
|
||||
foreach (var r in instances.Where(r => r is ILegacyRuleset))
|
||||
{
|
||||
if (realm.All<RulesetInfo>().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.OnlineID) == null)
|
||||
realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID));
|
||||
}
|
||||
|
||||
// add any other rulesets which have assemblies present but are not yet in the database.
|
||||
foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
|
||||
{
|
||||
if (rulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
|
||||
{
|
||||
var existingSameShortName = rulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName);
|
||||
|
||||
if (existingSameShortName != null)
|
||||
{
|
||||
// even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName.
|
||||
// this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one.
|
||||
// in such cases, update the instantiation info of the existing entry to point to the new one.
|
||||
existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo;
|
||||
}
|
||||
else
|
||||
realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID));
|
||||
}
|
||||
}
|
||||
|
||||
List<RulesetInfo> detachedRulesets = new List<RulesetInfo>();
|
||||
|
||||
// perform a consistency check and detach final rulesets from realm for cross-thread runtime usage.
|
||||
foreach (var r in rulesets.OrderBy(r => r.OnlineID))
|
||||
{
|
||||
try
|
||||
{
|
||||
var resolvedType = Type.GetType(r.InstantiationInfo)
|
||||
?? throw new RulesetLoadException(@"Type could not be resolved");
|
||||
|
||||
var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo
|
||||
?? throw new RulesetLoadException(@"Instantiation failure");
|
||||
|
||||
// If a ruleset isn't up-to-date with the API, it could cause a crash at an arbitrary point of execution.
|
||||
// To eagerly handle cases of missing implementations, enumerate all types here and mark as non-available on throw.
|
||||
resolvedType.Assembly.GetTypes();
|
||||
|
||||
r.Name = instanceInfo.Name;
|
||||
r.ShortName = instanceInfo.ShortName;
|
||||
r.InstantiationInfo = instanceInfo.InstantiationInfo;
|
||||
r.Available = true;
|
||||
|
||||
detachedRulesets.Add(r.Clone());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
r.Available = false;
|
||||
Logger.Log($"Could not load ruleset {r}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
availableRulesets.AddRange(detachedRulesets.OrderBy(r => r));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -201,7 +201,7 @@ namespace osu.Game.Rulesets
|
||||
/// Creates a <see cref="ScoreProcessor"/> for this <see cref="Ruleset"/>.
|
||||
/// </summary>
|
||||
/// <returns>The score processor.</returns>
|
||||
public virtual ScoreProcessor CreateScoreProcessor() => new ScoreProcessor();
|
||||
public virtual ScoreProcessor CreateScoreProcessor() => new ScoreProcessor(this);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="HealthProcessor"/> for this <see cref="Ruleset"/>.
|
||||
@ -228,25 +228,9 @@ namespace osu.Game.Rulesets
|
||||
/// <summary>
|
||||
/// Optionally creates a <see cref="PerformanceCalculator"/> to generate performance data from the provided score.
|
||||
/// </summary>
|
||||
/// <param name="attributes">Difficulty attributes for the beatmap related to the provided score.</param>
|
||||
/// <param name="score">The score to be processed.</param>
|
||||
/// <returns>A performance calculator instance for the provided score.</returns>
|
||||
[CanBeNull]
|
||||
public virtual PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => null;
|
||||
|
||||
/// <summary>
|
||||
/// Optionally creates a <see cref="PerformanceCalculator"/> to generate performance data from the provided score.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap to use as a source for generating <see cref="DifficultyAttributes"/>.</param>
|
||||
/// <param name="score">The score to be processed.</param>
|
||||
/// <returns>A performance calculator instance for the provided score.</returns>
|
||||
[CanBeNull]
|
||||
public PerformanceCalculator CreatePerformanceCalculator(IWorkingBeatmap beatmap, ScoreInfo score)
|
||||
{
|
||||
var difficultyCalculator = CreateDifficultyCalculator(beatmap);
|
||||
var difficultyAttributes = difficultyCalculator.Calculate(score.Mods);
|
||||
return CreatePerformanceCalculator(difficultyAttributes, score);
|
||||
}
|
||||
public virtual PerformanceCalculator CreatePerformanceCalculator() => null;
|
||||
|
||||
public virtual HitObjectComposer CreateHitObjectComposer() => null;
|
||||
|
||||
|
@ -7,34 +7,26 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Rulesets
|
||||
{
|
||||
public class RulesetStore : IDisposable, IRulesetStore
|
||||
public abstract class RulesetStore : IDisposable, IRulesetStore
|
||||
{
|
||||
private readonly RealmAccess realmAccess;
|
||||
|
||||
private const string ruleset_library_prefix = @"osu.Game.Rulesets";
|
||||
|
||||
private readonly Dictionary<Assembly, Type> loadedAssemblies = new Dictionary<Assembly, Type>();
|
||||
protected readonly Dictionary<Assembly, Type> LoadedAssemblies = new Dictionary<Assembly, Type>();
|
||||
|
||||
/// <summary>
|
||||
/// All available rulesets.
|
||||
/// </summary>
|
||||
public IEnumerable<RulesetInfo> AvailableRulesets => availableRulesets;
|
||||
public abstract IEnumerable<RulesetInfo> AvailableRulesets { get; }
|
||||
|
||||
private readonly List<RulesetInfo> availableRulesets = new List<RulesetInfo>();
|
||||
|
||||
public RulesetStore(RealmAccess realm, Storage? storage = null)
|
||||
protected RulesetStore(Storage? storage = null)
|
||||
{
|
||||
realmAccess = realm;
|
||||
|
||||
// On android in release configuration assemblies are loaded from the apk directly into memory.
|
||||
// We cannot read assemblies from cwd, so should check loaded assemblies instead.
|
||||
loadFromAppDomain();
|
||||
@ -53,8 +45,6 @@ namespace osu.Game.Rulesets
|
||||
var rulesetStorage = storage?.GetStorageForDirectory(@"rulesets");
|
||||
if (rulesetStorage != null)
|
||||
loadUserRulesets(rulesetStorage);
|
||||
|
||||
addMissingRulesets();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -95,80 +85,7 @@ namespace osu.Game.Rulesets
|
||||
if (domainAssembly != null)
|
||||
return domainAssembly;
|
||||
|
||||
return loadedAssemblies.Keys.FirstOrDefault(a => a.FullName == asm.FullName);
|
||||
}
|
||||
|
||||
private void addMissingRulesets()
|
||||
{
|
||||
realmAccess.Write(realm =>
|
||||
{
|
||||
var rulesets = realm.All<RulesetInfo>();
|
||||
|
||||
List<Ruleset> instances = loadedAssemblies.Values
|
||||
.Select(r => Activator.CreateInstance(r) as Ruleset)
|
||||
.Where(r => r != null)
|
||||
.Select(r => r.AsNonNull())
|
||||
.ToList();
|
||||
|
||||
// add all legacy rulesets first to ensure they have exclusive choice of primary key.
|
||||
foreach (var r in instances.Where(r => r is ILegacyRuleset))
|
||||
{
|
||||
if (realm.All<RulesetInfo>().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.OnlineID) == null)
|
||||
realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID));
|
||||
}
|
||||
|
||||
// add any other rulesets which have assemblies present but are not yet in the database.
|
||||
foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
|
||||
{
|
||||
if (rulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
|
||||
{
|
||||
var existingSameShortName = rulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName);
|
||||
|
||||
if (existingSameShortName != null)
|
||||
{
|
||||
// even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName.
|
||||
// this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one.
|
||||
// in such cases, update the instantiation info of the existing entry to point to the new one.
|
||||
existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo;
|
||||
}
|
||||
else
|
||||
realm.Add(new RulesetInfo(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.OnlineID));
|
||||
}
|
||||
}
|
||||
|
||||
List<RulesetInfo> detachedRulesets = new List<RulesetInfo>();
|
||||
|
||||
// perform a consistency check and detach final rulesets from realm for cross-thread runtime usage.
|
||||
foreach (var r in rulesets.OrderBy(r => r.OnlineID))
|
||||
{
|
||||
try
|
||||
{
|
||||
var resolvedType = Type.GetType(r.InstantiationInfo)
|
||||
?? throw new RulesetLoadException(@"Type could not be resolved");
|
||||
|
||||
var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo
|
||||
?? throw new RulesetLoadException(@"Instantiation failure");
|
||||
|
||||
// If a ruleset isn't up-to-date with the API, it could cause a crash at an arbitrary point of execution.
|
||||
// To eagerly handle cases of missing implementations, enumerate all types here and mark as non-available on throw.
|
||||
resolvedType.Assembly.GetTypes();
|
||||
|
||||
r.Name = instanceInfo.Name;
|
||||
r.ShortName = instanceInfo.ShortName;
|
||||
r.InstantiationInfo = instanceInfo.InstantiationInfo;
|
||||
r.Available = true;
|
||||
|
||||
detachedRulesets.Add(r.Clone());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
r.Available = false;
|
||||
Logger.Log($"Could not load ruleset {r}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
availableRulesets.AddRange(detachedRulesets.OrderBy(r => r));
|
||||
});
|
||||
return LoadedAssemblies.Keys.FirstOrDefault(a => a.FullName == asm.FullName);
|
||||
}
|
||||
|
||||
private void loadFromAppDomain()
|
||||
@ -214,7 +131,7 @@ namespace osu.Game.Rulesets
|
||||
{
|
||||
string? filename = Path.GetFileNameWithoutExtension(file);
|
||||
|
||||
if (loadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename))
|
||||
if (LoadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename))
|
||||
return;
|
||||
|
||||
try
|
||||
@ -229,17 +146,17 @@ namespace osu.Game.Rulesets
|
||||
|
||||
private void addRuleset(Assembly assembly)
|
||||
{
|
||||
if (loadedAssemblies.ContainsKey(assembly))
|
||||
if (LoadedAssemblies.ContainsKey(assembly))
|
||||
return;
|
||||
|
||||
// the same assembly may be loaded twice in the same AppDomain (currently a thing in certain Rider versions https://youtrack.jetbrains.com/issue/RIDER-48799).
|
||||
// as a failsafe, also compare by FullName.
|
||||
if (loadedAssemblies.Any(a => a.Key.FullName == assembly.FullName))
|
||||
if (LoadedAssemblies.Any(a => a.Key.FullName == assembly.FullName))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
loadedAssemblies[assembly] = assembly.GetTypes().First(t => t.IsPublic && t.IsSubclassOf(typeof(Ruleset)));
|
||||
LoadedAssemblies[assembly] = assembly.GetTypes().First(t => t.IsPublic && t.IsSubclassOf(typeof(Ruleset)));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
@ -1,6 +1,8 @@
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Utils;
|
||||
@ -14,12 +16,12 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// Invoked when the <see cref="ScoreProcessor"/> is in a failed state.
|
||||
/// Return true if the fail was permitted.
|
||||
/// </summary>
|
||||
public event Func<bool> Failed;
|
||||
public event Func<bool>? Failed;
|
||||
|
||||
/// <summary>
|
||||
/// Additional conditions on top of <see cref="DefaultFailCondition"/> that cause a failing state.
|
||||
/// </summary>
|
||||
public event Func<HealthProcessor, JudgementResult, bool> FailConditions;
|
||||
public event Func<HealthProcessor, JudgementResult, bool>? FailConditions;
|
||||
|
||||
/// <summary>
|
||||
/// The current health.
|
||||
|
@ -22,6 +22,23 @@ namespace osu.Game.Rulesets.Scoring
|
||||
return 10 * standardDeviation(timeOffsets);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the average hit offset/error for a sequence of <see cref="HitEvent"/>s, where negative numbers mean the user hit too early on average.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A non-null <see langword="double"/> value if unstable rate could be calculated,
|
||||
/// and <see langword="null"/> if unstable rate cannot be calculated due to <paramref name="hitEvents"/> being empty.
|
||||
/// </returns>
|
||||
public static double? CalculateAverageHitError(this IEnumerable<HitEvent> hitEvents)
|
||||
{
|
||||
double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset).ToArray();
|
||||
|
||||
if (timeOffsets.Length == 0)
|
||||
return null;
|
||||
|
||||
return timeOffsets.Average();
|
||||
}
|
||||
|
||||
private static bool affectsUnstableRate(HitEvent e) => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit();
|
||||
|
||||
private static double? standardDeviation(double[] timeOffsets)
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using osu.Framework.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring
|
||||
@ -16,6 +17,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// Indicates that the object has not been judged yet.
|
||||
/// </summary>
|
||||
[Description(@"")]
|
||||
[EnumMember(Value = "none")]
|
||||
[Order(14)]
|
||||
None,
|
||||
|
||||
@ -27,32 +29,39 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// "too far in the future). It should also define when a forced miss should be triggered (as a result of no user input in time).
|
||||
/// </remarks>
|
||||
[Description(@"Miss")]
|
||||
[EnumMember(Value = "miss")]
|
||||
[Order(5)]
|
||||
Miss,
|
||||
|
||||
[Description(@"Meh")]
|
||||
[EnumMember(Value = "meh")]
|
||||
[Order(4)]
|
||||
Meh,
|
||||
|
||||
[Description(@"OK")]
|
||||
[EnumMember(Value = "ok")]
|
||||
[Order(3)]
|
||||
Ok,
|
||||
|
||||
[Description(@"Good")]
|
||||
[EnumMember(Value = "good")]
|
||||
[Order(2)]
|
||||
Good,
|
||||
|
||||
[Description(@"Great")]
|
||||
[EnumMember(Value = "great")]
|
||||
[Order(1)]
|
||||
Great,
|
||||
|
||||
[Description(@"Perfect")]
|
||||
[EnumMember(Value = "perfect")]
|
||||
[Order(0)]
|
||||
Perfect,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates small tick miss.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "small_tick_miss")]
|
||||
[Order(11)]
|
||||
SmallTickMiss,
|
||||
|
||||
@ -60,12 +69,14 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// Indicates a small tick hit.
|
||||
/// </summary>
|
||||
[Description(@"S Tick")]
|
||||
[EnumMember(Value = "small_tick_hit")]
|
||||
[Order(7)]
|
||||
SmallTickHit,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates a large tick miss.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "large_tick_miss")]
|
||||
[Order(10)]
|
||||
LargeTickMiss,
|
||||
|
||||
@ -73,6 +84,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// Indicates a large tick hit.
|
||||
/// </summary>
|
||||
[Description(@"L Tick")]
|
||||
[EnumMember(Value = "large_tick_hit")]
|
||||
[Order(6)]
|
||||
LargeTickHit,
|
||||
|
||||
@ -80,6 +92,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// Indicates a small bonus.
|
||||
/// </summary>
|
||||
[Description("S Bonus")]
|
||||
[EnumMember(Value = "small_bonus")]
|
||||
[Order(9)]
|
||||
SmallBonus,
|
||||
|
||||
@ -87,18 +100,21 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// Indicates a large bonus.
|
||||
/// </summary>
|
||||
[Description("L Bonus")]
|
||||
[EnumMember(Value = "large_bonus")]
|
||||
[Order(8)]
|
||||
LargeBonus,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates a miss that should be ignored for scoring purposes.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "ignore_miss")]
|
||||
[Order(13)]
|
||||
IgnoreMiss,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates a hit that should be ignored for scoring purposes.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "ignore_hit")]
|
||||
[Order(12)]
|
||||
IgnoreHit,
|
||||
}
|
||||
@ -106,7 +122,19 @@ namespace osu.Game.Rulesets.Scoring
|
||||
public static class HitResultExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether a <see cref="HitResult"/> increases/decreases the combo, and affects the combo portion of the score.
|
||||
/// Whether a <see cref="HitResult"/> increases the combo.
|
||||
/// </summary>
|
||||
public static bool IncreasesCombo(this HitResult result)
|
||||
=> AffectsCombo(result) && IsHit(result);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a <see cref="HitResult"/> breaks the combo and resets it back to zero.
|
||||
/// </summary>
|
||||
public static bool BreaksCombo(this HitResult result)
|
||||
=> AffectsCombo(result) && !IsHit(result);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a <see cref="HitResult"/> increases/breaks the combo, and affects the combo portion of the score.
|
||||
/// </summary>
|
||||
public static bool AffectsCombo(this HitResult result)
|
||||
{
|
||||
@ -133,6 +161,30 @@ namespace osu.Game.Rulesets.Scoring
|
||||
public static bool AffectsAccuracy(this HitResult result)
|
||||
=> IsScorable(result) && !IsBonus(result);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a <see cref="HitResult"/> is a non-tick and non-bonus result.
|
||||
/// </summary>
|
||||
public static bool IsBasic(this HitResult result)
|
||||
=> IsScorable(result) && !IsTick(result) && !IsBonus(result);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a <see cref="HitResult"/> should be counted as a tick.
|
||||
/// </summary>
|
||||
public static bool IsTick(this HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.LargeTickHit:
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.SmallTickHit:
|
||||
case HitResult.SmallTickMiss:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a <see cref="HitResult"/> should be counted as bonus score.
|
||||
/// </summary>
|
||||
|
@ -1,8 +1,11 @@
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -17,12 +20,12 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// <summary>
|
||||
/// Invoked when a new judgement has occurred. This occurs after the judgement has been processed by this <see cref="JudgementProcessor"/>.
|
||||
/// </summary>
|
||||
public event Action<JudgementResult> NewJudgement;
|
||||
public event Action<JudgementResult>? NewJudgement;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a judgement is reverted, usually due to rewinding gameplay.
|
||||
/// </summary>
|
||||
public event Action<JudgementResult> JudgementReverted;
|
||||
public event Action<JudgementResult>? JudgementReverted;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of hits that can be judged.
|
||||
@ -34,7 +37,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
public int JudgedHits { get; private set; }
|
||||
|
||||
private JudgementResult lastAppliedResult;
|
||||
private JudgementResult? lastAppliedResult;
|
||||
|
||||
private readonly BindableBool hasCompleted = new BindableBool();
|
||||
|
||||
@ -163,7 +166,12 @@ namespace osu.Game.Rulesets.Scoring
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
hasCompleted.Value = JudgedHits == MaxHits && (JudgedHits == 0 || lastAppliedResult.TimeAbsolute < Clock.CurrentTime);
|
||||
|
||||
hasCompleted.Value =
|
||||
JudgedHits == MaxHits
|
||||
&& (JudgedHits == 0
|
||||
// Last applied result is guaranteed to be non-null when JudgedHits > 0.
|
||||
|| lastAppliedResult.AsNonNull().TimeAbsolute < Clock.CurrentTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,12 +1,15 @@
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -23,7 +26,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// <summary>
|
||||
/// Invoked when this <see cref="ScoreProcessor"/> was reset from a replay frame.
|
||||
/// </summary>
|
||||
public event Action OnResetFromReplayFrame;
|
||||
public event Action? OnResetFromReplayFrame;
|
||||
|
||||
/// <summary>
|
||||
/// The current total score.
|
||||
@ -76,6 +79,12 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
protected virtual double DefaultComboPortion => 0.7;
|
||||
|
||||
/// <summary>
|
||||
/// An arbitrary multiplier to scale scores in the <see cref="ScoringMode.Classic"/> scoring mode.
|
||||
/// </summary>
|
||||
protected virtual double ClassicScoreMultiplier => 36;
|
||||
|
||||
private readonly Ruleset ruleset;
|
||||
private readonly double accuracyPortion;
|
||||
private readonly double comboPortion;
|
||||
|
||||
@ -86,16 +95,32 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
private double maxBaseScore;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of basic (non-tick and non-bonus) hitobjects.
|
||||
/// </summary>
|
||||
private int maxBasicHitObjects;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum <see cref="HitResult"/> of a basic (non-tick and non-bonus) hitobject.
|
||||
/// Only populated via <see cref="ComputeFinalScore"/> or <see cref="ResetFromReplayFrame"/>.
|
||||
/// </summary>
|
||||
private HitResult? maxBasicResult;
|
||||
|
||||
private double rollingMaxBaseScore;
|
||||
private double baseScore;
|
||||
private int basicHitObjects;
|
||||
private bool beatmapApplied;
|
||||
|
||||
private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>();
|
||||
private readonly List<HitEvent> hitEvents = new List<HitEvent>();
|
||||
private HitObject lastHitObject;
|
||||
private HitObject? lastHitObject;
|
||||
|
||||
private double scoreMultiplier = 1;
|
||||
|
||||
public ScoreProcessor()
|
||||
public ScoreProcessor(Ruleset ruleset)
|
||||
{
|
||||
this.ruleset = ruleset;
|
||||
|
||||
accuracyPortion = DefaultAccuracyPortion;
|
||||
comboPortion = DefaultComboPortion;
|
||||
|
||||
@ -122,7 +147,11 @@ namespace osu.Game.Rulesets.Scoring
|
||||
};
|
||||
}
|
||||
|
||||
private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>();
|
||||
public override void ApplyBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
base.ApplyBeatmap(beatmap);
|
||||
beatmapApplied = true;
|
||||
}
|
||||
|
||||
protected sealed override void ApplyResultInternal(JudgementResult result)
|
||||
{
|
||||
@ -137,20 +166,10 @@ namespace osu.Game.Rulesets.Scoring
|
||||
if (!result.Type.IsScorable())
|
||||
return;
|
||||
|
||||
if (result.Type.AffectsCombo())
|
||||
{
|
||||
switch (result.Type)
|
||||
{
|
||||
case HitResult.Miss:
|
||||
case HitResult.LargeTickMiss:
|
||||
Combo.Value = 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
Combo.Value++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (result.Type.IncreasesCombo())
|
||||
Combo.Value++;
|
||||
else if (result.Type.BreaksCombo())
|
||||
Combo.Value = 0;
|
||||
|
||||
double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0;
|
||||
|
||||
@ -160,6 +179,9 @@ namespace osu.Game.Rulesets.Scoring
|
||||
rollingMaxBaseScore += result.Judgement.MaxNumericResult;
|
||||
}
|
||||
|
||||
if (result.Type.IsBasic())
|
||||
basicHitObjects++;
|
||||
|
||||
hitEvents.Add(CreateHitEvent(result));
|
||||
lastHitObject = result.HitObject;
|
||||
|
||||
@ -195,6 +217,9 @@ namespace osu.Game.Rulesets.Scoring
|
||||
rollingMaxBaseScore -= result.Judgement.MaxNumericResult;
|
||||
}
|
||||
|
||||
if (result.Type.IsBasic())
|
||||
basicHitObjects--;
|
||||
|
||||
Debug.Assert(hitEvents.Count > 0);
|
||||
lastHitObject = hitEvents[^1].LastHitObject;
|
||||
hitEvents.RemoveAt(hitEvents.Count - 1);
|
||||
@ -204,30 +229,122 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
private void updateScore()
|
||||
{
|
||||
if (rollingMaxBaseScore != 0)
|
||||
Accuracy.Value = calculateAccuracyRatio(baseScore, true);
|
||||
double rollingAccuracyRatio = rollingMaxBaseScore > 0 ? baseScore / rollingMaxBaseScore : 1;
|
||||
double accuracyRatio = maxBaseScore > 0 ? baseScore / maxBaseScore : 1;
|
||||
double comboRatio = maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1;
|
||||
|
||||
TotalScore.Value = getScore(Mode.Value);
|
||||
}
|
||||
|
||||
private double getScore(ScoringMode mode)
|
||||
{
|
||||
return GetScore(mode, maxAchievableCombo,
|
||||
calculateAccuracyRatio(baseScore),
|
||||
calculateComboRatio(HighestCombo.Value),
|
||||
scoreResultCounts);
|
||||
Accuracy.Value = rollingAccuracyRatio;
|
||||
TotalScore.Value = ComputeScore(Mode.Value, accuracyRatio, comboRatio, getBonusScore(scoreResultCounts), maxBasicHitObjects);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the total score.
|
||||
/// Computes the total score of a given finalised <see cref="ScoreInfo"/>. This should be used when a score is known to be complete.
|
||||
/// </summary>
|
||||
/// <param name="mode">The <see cref="ScoringMode"/> to compute the total score in.</param>
|
||||
/// <param name="maxCombo">The maximum combo achievable in the beatmap.</param>
|
||||
/// <remarks>
|
||||
/// Does not require <see cref="JudgementProcessor.ApplyBeatmap"/> to have been called before use.
|
||||
/// </remarks>
|
||||
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
|
||||
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
|
||||
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
|
||||
public double ComputeFinalScore(ScoringMode mode, ScoreInfo scoreInfo)
|
||||
{
|
||||
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
||||
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
|
||||
|
||||
extractFromStatistics(ruleset,
|
||||
scoreInfo.Statistics,
|
||||
out double extractedBaseScore,
|
||||
out double extractedMaxBaseScore,
|
||||
out int extractedMaxCombo,
|
||||
out int extractedBasicHitObjects);
|
||||
|
||||
double accuracyRatio = extractedMaxBaseScore > 0 ? extractedBaseScore / extractedMaxBaseScore : 1;
|
||||
double comboRatio = extractedMaxCombo > 0 ? (double)scoreInfo.MaxCombo / extractedMaxCombo : 1;
|
||||
|
||||
return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), extractedBasicHitObjects);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the total score of a partially-completed <see cref="ScoreInfo"/>. This should be used when it is unknown whether a score is complete.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Requires <see cref="JudgementProcessor.ApplyBeatmap"/> to have been called before use.
|
||||
/// </remarks>
|
||||
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
|
||||
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
|
||||
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
|
||||
public double ComputePartialScore(ScoringMode mode, ScoreInfo scoreInfo)
|
||||
{
|
||||
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
||||
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
|
||||
|
||||
if (!beatmapApplied)
|
||||
throw new InvalidOperationException($"Cannot compute partial score without calling {nameof(ApplyBeatmap)}.");
|
||||
|
||||
extractFromStatistics(ruleset,
|
||||
scoreInfo.Statistics,
|
||||
out double extractedBaseScore,
|
||||
out _,
|
||||
out _,
|
||||
out _);
|
||||
|
||||
double accuracyRatio = maxBaseScore > 0 ? extractedBaseScore / maxBaseScore : 1;
|
||||
double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1;
|
||||
|
||||
return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), maxBasicHitObjects);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the total score of a given <see cref="ScoreInfo"/> with a given custom max achievable combo.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is useful for processing legacy scores in which the maximum achievable combo can be more accurately determined via external means (e.g. database values or difficulty calculation).
|
||||
/// <p>Does not require <see cref="JudgementProcessor.ApplyBeatmap"/> to have been called before use.</p>
|
||||
/// </remarks>
|
||||
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
|
||||
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
|
||||
/// <param name="maxAchievableCombo">The maximum achievable combo for the provided beatmap.</param>
|
||||
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
|
||||
public double ComputeFinalLegacyScore(ScoringMode mode, ScoreInfo scoreInfo, int maxAchievableCombo)
|
||||
{
|
||||
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
||||
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
|
||||
|
||||
double accuracyRatio = scoreInfo.Accuracy;
|
||||
double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1;
|
||||
|
||||
// For legacy osu!mania scores, a full-GREAT score has 100% accuracy. If combined with a full-combo, the score becomes indistinguishable from a full-PERFECT score.
|
||||
// To get around this, the accuracy ratio is always recalculated based on the hit statistics rather than trusting the score.
|
||||
// Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together.
|
||||
if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3)
|
||||
{
|
||||
extractFromStatistics(
|
||||
ruleset,
|
||||
scoreInfo.Statistics,
|
||||
out double computedBaseScore,
|
||||
out double computedMaxBaseScore,
|
||||
out _,
|
||||
out _);
|
||||
|
||||
if (computedMaxBaseScore > 0)
|
||||
accuracyRatio = computedBaseScore / computedMaxBaseScore;
|
||||
}
|
||||
|
||||
int computedBasicHitObjects = scoreInfo.Statistics.Where(kvp => kvp.Key.IsBasic()).Select(kvp => kvp.Value).Sum();
|
||||
|
||||
return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), computedBasicHitObjects);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the total score from individual scoring components.
|
||||
/// </summary>
|
||||
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
|
||||
/// <param name="accuracyRatio">The accuracy percentage achieved by the player.</param>
|
||||
/// <param name="comboRatio">The proportion of <paramref name="maxCombo"/> achieved by the player.</param>
|
||||
/// <param name="statistics">Any statistics to be factored in.</param>
|
||||
/// <returns>The total score.</returns>
|
||||
public double GetScore(ScoringMode mode, int maxCombo, double accuracyRatio, double comboRatio, Dictionary<HitResult, int> statistics)
|
||||
/// <param name="comboRatio">The portion of the max combo achieved by the player.</param>
|
||||
/// <param name="bonusScore">The total bonus score.</param>
|
||||
/// <param name="totalBasicHitObjects">The total number of basic (non-tick and non-bonus) hitobjects in the beatmap.</param>
|
||||
/// <returns>The total score computed from the given scoring component ratios.</returns>
|
||||
public double ComputeScore(ScoringMode mode, double accuracyRatio, double comboRatio, double bonusScore, int totalBasicHitObjects)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
@ -235,56 +352,22 @@ namespace osu.Game.Rulesets.Scoring
|
||||
case ScoringMode.Standardised:
|
||||
double accuracyScore = accuracyPortion * accuracyRatio;
|
||||
double comboScore = comboPortion * comboRatio;
|
||||
return (max_score * (accuracyScore + comboScore) + getBonusScore(statistics)) * scoreMultiplier;
|
||||
return (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier;
|
||||
|
||||
case ScoringMode.Classic:
|
||||
// This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring.
|
||||
// The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes.
|
||||
double scaledStandardised = GetScore(ScoringMode.Standardised, maxCombo, accuracyRatio, comboRatio, statistics) / max_score;
|
||||
return Math.Pow(scaledStandardised * (maxCombo + 1), 2) * 18;
|
||||
double scaledStandardised = ComputeScore(ScoringMode.Standardised, accuracyRatio, comboRatio, bonusScore, totalBasicHitObjects) / max_score;
|
||||
return Math.Pow(scaledStandardised * Math.Max(1, totalBasicHitObjects), 2) * ClassicScoreMultiplier;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a minimal set of inputs, return the computed score for the tracked beatmap / mods combination, at the current point in time.
|
||||
/// Calculates the total bonus score from score statistics.
|
||||
/// </summary>
|
||||
/// <param name="mode">The <see cref="ScoringMode"/> to compute the total score in.</param>
|
||||
/// <param name="maxCombo">The maximum combo achievable in the beatmap.</param>
|
||||
/// <param name="statistics">Statistics to be used for calculating accuracy, bonus score, etc.</param>
|
||||
/// <returns>The computed score for provided inputs.</returns>
|
||||
public double GetImmediateScore(ScoringMode mode, int maxCombo, Dictionary<HitResult, int> statistics)
|
||||
{
|
||||
// calculate base score from statistics pairs
|
||||
int computedBaseScore = 0;
|
||||
|
||||
foreach (var pair in statistics)
|
||||
{
|
||||
if (!pair.Key.AffectsAccuracy())
|
||||
continue;
|
||||
|
||||
computedBaseScore += Judgement.ToNumericResult(pair.Key) * pair.Value;
|
||||
}
|
||||
|
||||
return GetScore(mode, maxAchievableCombo, calculateAccuracyRatio(computedBaseScore), calculateComboRatio(maxCombo), statistics);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the accuracy fraction for the provided base score.
|
||||
/// </summary>
|
||||
/// <param name="baseScore">The score to be used for accuracy calculation.</param>
|
||||
/// <param name="preferRolling">Whether the rolling base score should be used (ie. for the current point in time based on Apply/Reverted results).</param>
|
||||
/// <returns>The computed accuracy.</returns>
|
||||
private double calculateAccuracyRatio(double baseScore, bool preferRolling = false)
|
||||
{
|
||||
if (preferRolling && rollingMaxBaseScore != 0)
|
||||
return baseScore / rollingMaxBaseScore;
|
||||
|
||||
return maxBaseScore > 0 ? baseScore / maxBaseScore : 1;
|
||||
}
|
||||
|
||||
private double calculateComboRatio(int maxCombo) => maxAchievableCombo > 0 ? (double)maxCombo / maxAchievableCombo : 1;
|
||||
|
||||
private double getBonusScore(Dictionary<HitResult, int> statistics)
|
||||
/// <param name="statistics">The score statistics.</param>
|
||||
/// <returns>The total bonus score.</returns>
|
||||
private double getBonusScore(IReadOnlyDictionary<HitResult, int> statistics)
|
||||
=> statistics.GetValueOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE
|
||||
+ statistics.GetValueOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE;
|
||||
|
||||
@ -306,8 +389,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
public int GetStatistic(HitResult result) => scoreResultCounts.GetValueOrDefault(result);
|
||||
|
||||
public double GetStandardisedScore() => getScore(ScoringMode.Standardised);
|
||||
|
||||
/// <summary>
|
||||
/// Resets this ScoreProcessor to a default state.
|
||||
/// </summary>
|
||||
@ -324,10 +405,12 @@ namespace osu.Game.Rulesets.Scoring
|
||||
{
|
||||
maxAchievableCombo = HighestCombo.Value;
|
||||
maxBaseScore = baseScore;
|
||||
maxBasicHitObjects = basicHitObjects;
|
||||
}
|
||||
|
||||
baseScore = 0;
|
||||
rollingMaxBaseScore = 0;
|
||||
basicHitObjects = 0;
|
||||
|
||||
TotalScore.Value = 0;
|
||||
Accuracy.Value = 1;
|
||||
@ -341,23 +424,19 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
public virtual void PopulateScore(ScoreInfo score)
|
||||
{
|
||||
score.TotalScore = (long)Math.Round(GetStandardisedScore());
|
||||
score.Combo = Combo.Value;
|
||||
score.MaxCombo = HighestCombo.Value;
|
||||
score.Accuracy = Accuracy.Value;
|
||||
score.Rank = Rank.Value;
|
||||
score.HitEvents = hitEvents;
|
||||
|
||||
foreach (var result in HitResultExtensions.ALL_TYPES)
|
||||
score.Statistics[result] = GetStatistic(result);
|
||||
|
||||
score.HitEvents = hitEvents;
|
||||
// Populate total score after everything else.
|
||||
score.TotalScore = (long)Math.Round(ComputeFinalScore(ScoringMode.Standardised, score));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maximum <see cref="HitResult"/> for a normal hit (i.e. not tick/bonus) for this ruleset. Only populated via <see cref="ResetFromReplayFrame"/>.
|
||||
/// </summary>
|
||||
private HitResult? maxNormalResult;
|
||||
|
||||
public override void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame)
|
||||
{
|
||||
base.ResetFromReplayFrame(ruleset, frame);
|
||||
@ -365,11 +444,26 @@ namespace osu.Game.Rulesets.Scoring
|
||||
if (frame.Header == null)
|
||||
return;
|
||||
|
||||
baseScore = 0;
|
||||
rollingMaxBaseScore = 0;
|
||||
extractFromStatistics(ruleset, frame.Header.Statistics, out baseScore, out rollingMaxBaseScore, out _, out _);
|
||||
HighestCombo.Value = frame.Header.MaxCombo;
|
||||
|
||||
foreach ((HitResult result, int count) in frame.Header.Statistics)
|
||||
scoreResultCounts.Clear();
|
||||
scoreResultCounts.AddRange(frame.Header.Statistics);
|
||||
|
||||
updateScore();
|
||||
|
||||
OnResetFromReplayFrame?.Invoke();
|
||||
}
|
||||
|
||||
private void extractFromStatistics(Ruleset ruleset, IReadOnlyDictionary<HitResult, int> statistics, out double baseScore, out double maxBaseScore, out int maxCombo,
|
||||
out int basicHitObjects)
|
||||
{
|
||||
baseScore = 0;
|
||||
maxBaseScore = 0;
|
||||
maxCombo = 0;
|
||||
basicHitObjects = 0;
|
||||
|
||||
foreach ((HitResult result, int count) in statistics)
|
||||
{
|
||||
// Bonus scores are counted separately directly from the statistics dictionary later on.
|
||||
if (!result.IsScorable() || result.IsBonus())
|
||||
@ -392,20 +486,19 @@ namespace osu.Game.Rulesets.Scoring
|
||||
break;
|
||||
|
||||
default:
|
||||
maxResult = maxNormalResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result;
|
||||
maxResult = maxBasicResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result;
|
||||
break;
|
||||
}
|
||||
|
||||
baseScore += count * Judgement.ToNumericResult(result);
|
||||
rollingMaxBaseScore += count * Judgement.ToNumericResult(maxResult);
|
||||
maxBaseScore += count * Judgement.ToNumericResult(maxResult);
|
||||
|
||||
if (result.AffectsCombo())
|
||||
maxCombo += count;
|
||||
|
||||
if (result.IsBasic())
|
||||
basicHitObjects += count;
|
||||
}
|
||||
|
||||
scoreResultCounts.Clear();
|
||||
scoreResultCounts.AddRange(frame.Header.Statistics);
|
||||
|
||||
updateScore();
|
||||
|
||||
OnResetFromReplayFrame?.Invoke();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -133,6 +133,11 @@ namespace osu.Game.Rulesets.UI
|
||||
p.NewResult += (_, r) => NewResult?.Invoke(r);
|
||||
p.RevertResult += (_, r) => RevertResult?.Invoke(r);
|
||||
}));
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
IsPaused.ValueChanged += paused =>
|
||||
{
|
||||
|
109
osu.Game/Rulesets/UI/ModSwitchSmall.cs
Normal file
109
osu.Game/Rulesets/UI/ModSwitchSmall.cs
Normal file
@ -0,0 +1,109 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
public class ModSwitchSmall : CompositeDrawable
|
||||
{
|
||||
public BindableBool Active { get; } = new BindableBool();
|
||||
|
||||
public const float DEFAULT_SIZE = 60;
|
||||
|
||||
private readonly IMod mod;
|
||||
|
||||
private readonly SpriteIcon background;
|
||||
private readonly SpriteIcon? modIcon;
|
||||
|
||||
private Color4 activeForegroundColour;
|
||||
private Color4 inactiveForegroundColour;
|
||||
|
||||
private Color4 activeBackgroundColour;
|
||||
private Color4 inactiveBackgroundColour;
|
||||
|
||||
public ModSwitchSmall(IMod mod)
|
||||
{
|
||||
this.mod = mod;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
FillFlowContainer contentFlow;
|
||||
ModSwitchTiny tinySwitch;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
background = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(DEFAULT_SIZE),
|
||||
Icon = OsuIcon.ModBg
|
||||
},
|
||||
contentFlow = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Spacing = new Vector2(0, 4),
|
||||
Direction = FillDirection.Vertical,
|
||||
Child = tinySwitch = new ModSwitchTiny(mod)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Scale = new Vector2(0.6f),
|
||||
Active = { BindTarget = Active }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (mod.Icon != null)
|
||||
{
|
||||
contentFlow.Insert(-1, modIcon = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Size = new Vector2(21),
|
||||
Icon = mod.Icon.Value
|
||||
});
|
||||
tinySwitch.Scale = new Vector2(0.3f);
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OsuColour colours, OverlayColourProvider? colourProvider)
|
||||
{
|
||||
inactiveForegroundColour = colourProvider?.Background5 ?? colours.Gray3;
|
||||
activeForegroundColour = colours.ForModType(mod.Type);
|
||||
|
||||
inactiveBackgroundColour = colourProvider?.Background2 ?? colours.Gray5;
|
||||
activeBackgroundColour = Interpolation.ValueAt<Colour4>(0.1f, Colour4.Black, activeForegroundColour, 0, 1);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Active.BindValueChanged(_ => updateState(), true);
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
modIcon?.FadeColour(Active.Value ? activeForegroundColour : inactiveForegroundColour, 200, Easing.OutQuint);
|
||||
background.FadeColour(Active.Value ? activeBackgroundColour : inactiveBackgroundColour, 200, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
93
osu.Game/Rulesets/UI/ModSwitchTiny.cs
Normal file
93
osu.Game/Rulesets/UI/ModSwitchTiny.cs
Normal file
@ -0,0 +1,93 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
public class ModSwitchTiny : CompositeDrawable
|
||||
{
|
||||
public BindableBool Active { get; } = new BindableBool();
|
||||
|
||||
public const float DEFAULT_HEIGHT = 30;
|
||||
|
||||
private readonly IMod mod;
|
||||
|
||||
private readonly Box background;
|
||||
private readonly OsuSpriteText acronymText;
|
||||
|
||||
private Color4 activeForegroundColour;
|
||||
private Color4 inactiveForegroundColour;
|
||||
|
||||
private Color4 activeBackgroundColour;
|
||||
private Color4 inactiveBackgroundColour;
|
||||
|
||||
public ModSwitchTiny(IMod mod)
|
||||
{
|
||||
this.mod = mod;
|
||||
Size = new Vector2(73, DEFAULT_HEIGHT);
|
||||
|
||||
InternalChild = new CircularContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
acronymText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Shadow = false,
|
||||
Font = OsuFont.Numeric.With(size: 24, weight: FontWeight.Black),
|
||||
Text = mod.Acronym,
|
||||
Margin = new MarginPadding
|
||||
{
|
||||
Top = 4
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OsuColour colours, OverlayColourProvider? colourProvider)
|
||||
{
|
||||
inactiveBackgroundColour = colourProvider?.Background5 ?? colours.Gray3;
|
||||
activeBackgroundColour = colours.ForModType(mod.Type);
|
||||
|
||||
inactiveForegroundColour = colourProvider?.Background2 ?? colours.Gray5;
|
||||
activeForegroundColour = Interpolation.ValueAt<Colour4>(0.1f, Colour4.Black, activeForegroundColour, 0, 1);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Active.BindValueChanged(_ => updateState(), true);
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
acronymText.FadeColour(Active.Value ? activeForegroundColour : inactiveForegroundColour, 200, Easing.OutQuint);
|
||||
background.FadeColour(Active.Value ? activeBackgroundColour : inactiveBackgroundColour, 200, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,22 +3,22 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
@ -79,6 +79,11 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
private readonly List<Playfield> nestedPlayfields = new List<Playfield>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether this <see cref="Playfield"/> is nested in another <see cref="Playfield"/>.
|
||||
/// </summary>
|
||||
public bool IsNested { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether judgements should be displayed by this and and all nested <see cref="Playfield"/>s.
|
||||
/// </summary>
|
||||
@ -206,6 +211,8 @@ namespace osu.Game.Rulesets.UI
|
||||
/// <param name="otherPlayfield">The <see cref="Playfield"/> to add.</param>
|
||||
protected void AddNested(Playfield otherPlayfield)
|
||||
{
|
||||
otherPlayfield.IsNested = true;
|
||||
|
||||
otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements);
|
||||
|
||||
otherPlayfield.NewResult += (d, r) => NewResult?.Invoke(d, r);
|
||||
@ -229,7 +236,7 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (mods != null)
|
||||
if (!IsNested && mods != null)
|
||||
{
|
||||
foreach (var mod in mods)
|
||||
{
|
||||
@ -257,10 +264,25 @@ namespace osu.Game.Rulesets.UI
|
||||
var entry = CreateLifetimeEntry(hitObject);
|
||||
lifetimeEntryMap[entry.HitObject] = entry;
|
||||
|
||||
preloadSamples(hitObject);
|
||||
|
||||
HitObjectContainer.Add(entry);
|
||||
OnHitObjectAdded(entry.HitObject);
|
||||
}
|
||||
|
||||
private void preloadSamples(HitObject hitObject)
|
||||
{
|
||||
// prepare sample pools ahead of time so we're not initialising at runtime.
|
||||
foreach (var sample in hitObject.Samples)
|
||||
prepareSamplePool(hitObject.SampleControlPoint.ApplyTo(sample));
|
||||
|
||||
foreach (var sample in hitObject.AuxiliarySamples)
|
||||
prepareSamplePool(hitObject.SampleControlPoint.ApplyTo(sample));
|
||||
|
||||
foreach (var nestedObject in hitObject.NestedHitObjects)
|
||||
preloadSamples(nestedObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a <see cref="HitObjectLifetimeEntry"/> for a pooled <see cref="HitObject"/> from this <see cref="Playfield"/>.
|
||||
/// </summary>
|
||||
@ -301,7 +323,7 @@ namespace osu.Game.Rulesets.UI
|
||||
/// </param>
|
||||
/// <typeparam name="TObject">The <see cref="HitObject"/> type.</typeparam>
|
||||
/// <typeparam name="TDrawable">The <see cref="DrawableHitObject"/> receiver for <typeparamref name="TObject"/>s.</typeparam>
|
||||
protected void RegisterPool<TObject, TDrawable>(int initialSize, int? maximumSize = null)
|
||||
public void RegisterPool<TObject, TDrawable>(int initialSize, int? maximumSize = null)
|
||||
where TObject : HitObject
|
||||
where TDrawable : DrawableHitObject, new()
|
||||
=> RegisterPool<TObject, TDrawable>(new DrawablePool<TDrawable>(initialSize, maximumSize));
|
||||
@ -323,22 +345,7 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
DrawableHitObject IPooledHitObjectProvider.GetPooledDrawableRepresentation(HitObject hitObject, DrawableHitObject parent)
|
||||
{
|
||||
var lookupType = hitObject.GetType();
|
||||
|
||||
IDrawablePool pool;
|
||||
|
||||
// Tests may add derived hitobject instances for which pools don't exist. Try to find any applicable pool and dynamically assign the type if the pool exists.
|
||||
if (!pools.TryGetValue(lookupType, out pool))
|
||||
{
|
||||
foreach (var (t, p) in pools)
|
||||
{
|
||||
if (!t.IsInstanceOfType(hitObject))
|
||||
continue;
|
||||
|
||||
pools[lookupType] = pool = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
var pool = prepareDrawableHitObjectPool(hitObject);
|
||||
|
||||
return (DrawableHitObject)pool?.Get(d =>
|
||||
{
|
||||
@ -365,14 +372,39 @@ namespace osu.Game.Rulesets.UI
|
||||
});
|
||||
}
|
||||
|
||||
private IDrawablePool prepareDrawableHitObjectPool(HitObject hitObject)
|
||||
{
|
||||
var lookupType = hitObject.GetType();
|
||||
|
||||
IDrawablePool pool;
|
||||
|
||||
// Tests may add derived hitobject instances for which pools don't exist. Try to find any applicable pool and dynamically assign the type if the pool exists.
|
||||
if (!pools.TryGetValue(lookupType, out pool))
|
||||
{
|
||||
foreach (var (t, p) in pools)
|
||||
{
|
||||
if (!t.IsInstanceOfType(hitObject))
|
||||
continue;
|
||||
|
||||
pools[lookupType] = pool = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
private readonly Dictionary<ISampleInfo, DrawablePool<PoolableSkinnableSample>> samplePools = new Dictionary<ISampleInfo, DrawablePool<PoolableSkinnableSample>>();
|
||||
|
||||
public PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo)
|
||||
{
|
||||
if (!samplePools.TryGetValue(sampleInfo, out var existingPool))
|
||||
AddInternal(samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 1));
|
||||
public PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo) => prepareSamplePool(sampleInfo).Get();
|
||||
|
||||
return existingPool.Get();
|
||||
private DrawablePool<PoolableSkinnableSample> prepareSamplePool(ISampleInfo sampleInfo)
|
||||
{
|
||||
if (samplePools.TryGetValue(sampleInfo, out var pool)) return pool;
|
||||
|
||||
AddInternal(samplePools[sampleInfo] = pool = new DrawableSamplePool(sampleInfo, 1));
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
private class DrawableSamplePool : DrawablePool<PoolableSkinnableSample>
|
||||
|
Reference in New Issue
Block a user