diff --git a/README.md b/README.md
index eb2fe6d0eb..cf7ce35791 100644
--- a/README.md
+++ b/README.md
@@ -16,21 +16,20 @@ The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Curre
## Status
-This project is under heavy development, but is in a stable state. Users are encouraged to try it out and keep it installed alongside the stable *osu!* client. It will continue to evolve to the point of eventually replacing the existing stable client as an update.
+This project is under constant development, but we aim to keep things in a stable state. Users are encouraged to try it out and keep it installed alongside the stable *osu!* client. It will continue to evolve to the point of eventually replacing the existing stable client as an update.
-**IMPORTANT:** Gameplay mechanics (and other features which you may have come to know and love) are in a constant state of flux. Game balance and final quality-of-life passes come at the end of development, preceded by experimentation and changes which may potentially **reduce playability or usability**. This is done in order to allow us to move forward as developers and designers more efficiently. If this offends you, please consider sticking to the stable releases of osu! (found on the website). We are not yet open to heated discussion over game mechanics and will not be using github as a forum for such discussions just yet.
+**IMPORTANT:** Gameplay mechanics (and other features which you may have come to know and love) are in a constant state of flux. Game balance and final quality-of-life passes come at the end of development, preceded by experimentation and changes which may potentially **reduce playability or usability**. This is done in order to allow us to move forward as developers and designers more efficiently. If this offends you, please consider sticking to a [stable release](https://osu.ppy.sh/home/download) of osu!. We are not yet open to heated discussion over game mechanics and will not be using github as a forum for such discussions just yet.
We are accepting bug reports (please report with as much detail as possible and follow the existing issue templates). Feature requests are also welcome, but understand that our focus is on completing the game to feature parity before adding new features. A few resources are available as starting points to getting involved and understanding the project:
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
- You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management).
-- Read peppy's [blog post](https://blog.ppy.sh/a-definitive-lazer-faq/) exploring where the project is currently and the roadmap going forward.
## Running osu!
-If you are looking to install or test osu! without setting up a development environment, you can consume our [binary releases](https://github.com/ppy/osu/releases). Handy links below will download the latest version for your operating system of choice:
+If you are looking to install or test osu! without setting up a development environment, you can consume our [releases](https://github.com/ppy/osu/releases). You can also generally download a version for your current device from the [osu! site](https://osu.ppy.sh/home/download). Failing that, you may use the links below to download the latest version for your operating system of choice:
-**Latest build:**
+**Latest release:**
| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 10.15+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 13.4+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) |
| ------------- | ------------- | ------------- | ------------- | ------------- |
@@ -50,9 +49,8 @@ You can see some examples of custom rulesets by visiting the [custom ruleset dir
Please make sure you have the following prerequisites:
- A desktop platform with the [.NET 6.0 SDK](https://dotnet.microsoft.com/download) installed.
-- When developing with mobile, [Xamarin](https://docs.microsoft.com/en-us/xamarin/) is required, which is shipped together with Visual Studio or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/).
-- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
-- When running on Linux, please have a system-wide FFmpeg installation available to support video decoding.
+
+When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
### Downloading the source code
@@ -89,7 +87,29 @@ _Due to a historical feature gap between .NET Core and Xamarin, running `dotnet`
### Testing with resource/framework modifications
-Sometimes it may be necessary to cross-test changes in [osu-resources](https://github.com/ppy/osu-resources) or [osu-framework](https://github.com/ppy/osu-framework). This can be achieved by running some commands as documented on the [osu-resources](https://github.com/ppy/osu-resources/wiki/Testing-local-resources-checkout-with-other-projects) and [osu-framework](https://github.com/ppy/osu-framework/wiki/Testing-local-framework-checkout-with-other-projects) wiki pages.
+Sometimes it may be necessary to cross-test changes in [osu-resources](https://github.com/ppy/osu-resources) or [osu-framework](https://github.com/ppy/osu-framework). This can be quickly achieved using included commands:
+
+Windows:
+
+```ps
+UseLocalFramework.ps1
+UseLocalResources.ps1
+```
+
+macOS / Linux:
+
+```ps
+UseLocalFramework.sh
+UseLocalResources.sh
+```
+
+Note that these commands assume you have the relevant project(s) checked out in adjacent directories:
+
+```
+|- osu // this repository
+|- osu-framework
+|- osu-resources
+```
### Code analysis
diff --git a/osu.Android.props b/osu.Android.props
index 6aebae665d..c88bea8265 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -11,7 +11,7 @@
manifestmerger.jar
-
+
diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
index b6a42407da..9323296b7f 100644
--- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
@@ -1,17 +1,30 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Scoring
{
public partial class CatchScoreProcessor : ScoreProcessor
{
+ private const int combo_cap = 200;
+ private const double combo_base = 4;
+
public CatchScoreProcessor()
: base(new CatchRuleset())
{
}
- protected override double ClassicScoreMultiplier => 28;
+ protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
+ {
+ return 600000 * comboProgress
+ + 400000 * Accuracy.Value * accuracyProgress
+ + bonusPortion;
+ }
+
+ protected override double GetComboScoreChange(JudgementResult result)
+ => Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
}
}
diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
index 99a80ef28d..b2155968ea 100644
--- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
+++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
@@ -21,18 +21,29 @@ namespace osu.Game.Rulesets.Mania.Configuration
{
base.InitialiseDefaults();
- SetDefault(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 5);
+ SetDefault(ManiaRulesetSetting.ScrollSpeed, 8, 1, 40);
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false);
+
+#pragma warning disable CS0618
+ // Although obsolete, this is still required to populate the bindable from the database in case migration is required.
+ SetDefault(ManiaRulesetSetting.ScrollTime, null);
+
+ if (Get(ManiaRulesetSetting.ScrollTime) is double scrollTime)
+ {
+ SetValue(ManiaRulesetSetting.ScrollSpeed, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime));
+ SetValue(ManiaRulesetSetting.ScrollTime, null);
+ }
+#pragma warning restore CS0618
}
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
{
- new TrackedSetting(ManiaRulesetSetting.ScrollTime,
- scrollTime => new SettingDescription(
- rawValue: scrollTime,
+ new TrackedSetting(ManiaRulesetSetting.ScrollSpeed,
+ speed => new SettingDescription(
+ rawValue: speed,
name: RulesetSettingsStrings.ScrollSpeed,
- value: RulesetSettingsStrings.ScrollSpeedTooltip(scrollTime, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime))
+ value: RulesetSettingsStrings.ScrollSpeedTooltip(DrawableManiaRuleset.ComputeScrollTime(speed), speed)
)
)
};
@@ -40,7 +51,9 @@ namespace osu.Game.Rulesets.Mania.Configuration
public enum ManiaRulesetSetting
{
+ [Obsolete("Use ScrollSpeed instead.")] // Can be removed 2023-11-30
ScrollTime,
+ ScrollSpeed,
ScrollDirection,
TimingBasedNoteColouring
}
diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
index fc0b4a9ed9..a5434a36ab 100644
--- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
@@ -34,10 +33,10 @@ namespace osu.Game.Rulesets.Mania
LabelText = RulesetSettingsStrings.ScrollingDirection,
Current = config.GetBindable(ManiaRulesetSetting.ScrollDirection)
},
- new SettingsSlider
+ new SettingsSlider
{
LabelText = RulesetSettingsStrings.ScrollSpeed,
- Current = config.GetBindable(ManiaRulesetSetting.ScrollTime),
+ Current = config.GetBindable(ManiaRulesetSetting.ScrollSpeed),
KeyboardStep = 5
},
new SettingsCheckbox
@@ -48,9 +47,9 @@ namespace osu.Game.Rulesets.Mania
};
}
- private partial class ManiaScrollSlider : RoundedSliderBar
+ private partial class ManiaScrollSlider : RoundedSliderBar
{
- public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip(Current.Value, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / Current.Value));
+ public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip(DrawableManiaRuleset.ComputeScrollTime(Current.Value), Current.Value);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index ce34addeff..3f91328128 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -245,7 +245,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
// As the note is being held, adjust the size of the sizing container. This has two effects:
// 1. The contained masking container will mask the body and ticks.
// 2. The head note will move along with the new "head position" in the container.
- if (Head.IsHit && releaseTime == null && DrawHeight > 0)
+ //
+ // As per stable, this should not apply for early hits, waiting until the object starts to touch the
+ // judgement area first.
+ if (Head.IsHit && releaseTime == null && DrawHeight > 0 && Time.Current >= HitObject.StartTime)
{
// How far past the hit target this hold note is.
float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y;
diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
index f724972a29..3341f834dd 100644
--- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
+++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
@@ -1,23 +1,29 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Scoring
{
- internal partial class ManiaScoreProcessor : ScoreProcessor
+ public partial class ManiaScoreProcessor : ScoreProcessor
{
+ private const double combo_base = 4;
+
public ManiaScoreProcessor()
: base(new ManiaRuleset())
{
}
- protected override double DefaultAccuracyPortion => 0.99;
+ protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
+ {
+ return 200000 * comboProgress
+ + 800000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
+ + bonusPortion;
+ }
- protected override double DefaultComboPortion => 0.01;
-
- protected override double ClassicScoreMultiplier => 16;
+ protected override double GetComboScoreChange(JudgementResult result)
+ => Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base));
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index af8758fb5e..2d373c0471 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -33,12 +33,12 @@ namespace osu.Game.Rulesets.Mania.UI
public partial class DrawableManiaRuleset : DrawableScrollingRuleset
{
///
- /// The minimum time range. This occurs at a of 40.
+ /// The minimum time range. This occurs at a of 40.
///
public const double MIN_TIME_RANGE = 290;
///
- /// The maximum time range. This occurs at a of 1.
+ /// The maximum time range. This occurs with a of 1.
///
public const double MAX_TIME_RANGE = 11485;
@@ -69,7 +69,8 @@ namespace osu.Game.Rulesets.Mania.UI
protected override ScrollVisualisationMethod VisualisationMethod => scrollMethod;
private readonly Bindable configDirection = new Bindable();
- private readonly BindableDouble configTimeRange = new BindableDouble();
+ private readonly BindableInt configScrollSpeed = new BindableInt();
+ private double smoothTimeRange;
// Stores the current speed adjustment active in gameplay.
private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
@@ -78,6 +79,9 @@ namespace osu.Game.Rulesets.Mania.UI
: base(ruleset, beatmap, mods)
{
BarLines = new BarLineGenerator(Beatmap).BarLines;
+
+ TimeRange.MinValue = 1;
+ TimeRange.MaxValue = MAX_TIME_RANGE;
}
[BackgroundDependencyLoader]
@@ -104,30 +108,28 @@ namespace osu.Game.Rulesets.Mania.UI
Config.BindWith(ManiaRulesetSetting.ScrollDirection, configDirection);
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
- Config.BindWith(ManiaRulesetSetting.ScrollTime, configTimeRange);
- TimeRange.MinValue = configTimeRange.MinValue;
- TimeRange.MaxValue = configTimeRange.MaxValue;
+ Config.BindWith(ManiaRulesetSetting.ScrollSpeed, configScrollSpeed);
+ configScrollSpeed.BindValueChanged(speed => this.TransformTo(nameof(smoothTimeRange), ComputeScrollTime(speed.NewValue), 200, Easing.OutQuint));
+
+ TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value);
}
- protected override void AdjustScrollSpeed(int amount)
- {
- this.TransformTo(nameof(relativeTimeRange), relativeTimeRange + amount, 200, Easing.OutQuint);
- }
-
- private double relativeTimeRange
- {
- get => MAX_TIME_RANGE / configTimeRange.Value;
- set => configTimeRange.Value = MAX_TIME_RANGE / value;
- }
+ protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount;
protected override void Update()
{
base.Update();
-
updateTimeRange();
}
- private void updateTimeRange() => TimeRange.Value = configTimeRange.Value * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value;
+ private void updateTimeRange() => TimeRange.Value = smoothTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value;
+
+ ///
+ /// Computes a scroll time (in milliseconds) from a scroll speed in the range of 1-40.
+ ///
+ /// The scroll speed.
+ /// The scroll time.
+ public static double ComputeScrollTime(int scrollSpeed) => MAX_TIME_RANGE / scrollSpeed;
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer();
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
index 8fdab9f1f9..616a9c362d 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
@@ -17,6 +17,18 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModAutoplay : OsuModTestScene
{
+ [Test]
+ public void TestCursorPositionStoredToJudgement()
+ {
+ CreateModTest(new ModTestData
+ {
+ Autoplay = true,
+ PassCondition = () =>
+ Player.ScoreProcessor.JudgedHits >= 1
+ && Player.ScoreProcessor.HitEvents.Any(e => e.Position != null)
+ });
+ }
+
[Test]
public void TestSpmUnaffectedByRateAdjust()
=> runSpmTest(new OsuModDaycore
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
index 4782a0e49c..3841c9c716 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
@@ -24,7 +24,17 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation;
public override LocalisableString Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 0.1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModRepel) };
+
+ public override Type[] IncompatibleMods => new[]
+ {
+ typeof(OsuModSpunOut),
+ typeof(ModRelax),
+ typeof(ModFailCondition),
+ typeof(ModNoFail),
+ typeof(ModAutoplay),
+ typeof(OsuModMagnetised),
+ typeof(OsuModRepel)
+ };
public bool PerformFail() => false;
@@ -34,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Mods
private List replayFrames = null!;
- private int currentFrame;
+ private int currentFrame = -1;
public void Update(Playfield playfield)
{
@@ -43,8 +53,9 @@ namespace osu.Game.Rulesets.Osu.Mods
double time = playfield.Clock.CurrentTime;
// Very naive implementation of autopilot based on proximity to replay frames.
+ // Special case for the first frame is required to ensure the mouse is in a sane position until the actual time of the first frame is hit.
// TODO: this needs to be based on user interactions to better match stable (pausing until judgement is registered).
- if (Math.Abs(replayFrames[currentFrame + 1].Time - time) <= Math.Abs(replayFrames[currentFrame].Time - time))
+ if (currentFrame < 0 || Math.Abs(replayFrames[currentFrame + 1].Time - time) <= Math.Abs(replayFrames[currentFrame].Time - time))
{
currentFrame++;
new MousePositionAbsoluteInput { Position = playfield.ToScreenSpace(replayFrames[currentFrame].Position) }.Apply(inputManager.CurrentState, inputManager);
diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
index 50d4eb6258..f97be0d7ff 100644
--- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
+++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
@@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System;
using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements;
-using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
@@ -18,21 +15,14 @@ namespace osu.Game.Rulesets.Osu.Scoring
{
}
- protected override double ClassicScoreMultiplier => 36;
-
protected override HitEvent CreateHitEvent(JudgementResult result)
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
- protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement)
+ protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{
- switch (hitObject)
- {
- case HitCircle:
- return new OsuHitCircleJudgementResult(hitObject, judgement);
-
- default:
- return new OsuJudgementResult(hitObject, judgement);
- }
+ return 700000 * comboProgress
+ + 300000 * Math.Pow(Accuracy.Value, 10) * accuracyProgress
+ + bonusPortion;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
index e4e68c7207..dd8748f6e3 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
@@ -91,8 +91,9 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
prepareDrawableRulesetAndBeatmap(false);
- assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
- assertStateAfterResult(new JudgementResult(new Hit.StrongNestedHit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle);
+ var hit = new Hit();
+ assertStateAfterResult(new JudgementResult(hit, new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
+ assertStateAfterResult(new JudgementResult(new Hit.StrongNestedHit(hit), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle);
}
[Test]
diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
index aa5da6d710..2f4a98bd8f 100644
--- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
@@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
- protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit
+ protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit(this)
{
StartTime = startTime,
Samples = Samples
@@ -117,6 +117,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
{
// The strong hit of the drum roll doesn't actually provide any score.
public override Judgement CreateJudgement() => new IgnoreJudgement();
+
+ public StrongNestedHit(TaikoHitObject parent)
+ : base(parent)
+ {
+ }
}
#region LegacyBeatmapEncoder
diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs
index f8203d793d..206e8ecb5a 100644
--- a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
public override double MaximumJudgementOffset => HitWindow;
- protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit
+ protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit(this)
{
StartTime = startTime,
Samples = Samples
@@ -41,6 +41,10 @@ namespace osu.Game.Rulesets.Taiko.Objects
public class StrongNestedHit : StrongNestedHitObject
{
+ public StrongNestedHit(TaikoHitObject parent)
+ : base(parent)
+ {
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Hit.cs b/osu.Game.Rulesets.Taiko/Objects/Hit.cs
index 8935878f0e..ec23079ed9 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Hit.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Hit.cs
@@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
}
}
- protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit
+ protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit(this)
{
StartTime = startTime,
Samples = Samples
@@ -80,6 +80,10 @@ namespace osu.Game.Rulesets.Taiko.Objects
public class StrongNestedHit : StrongNestedHitObject
{
+ public StrongNestedHit(TaikoHitObject parent)
+ : base(parent)
+ {
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs
index 628c41d878..316115f44d 100644
--- a/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs
@@ -15,6 +15,13 @@ namespace osu.Game.Rulesets.Taiko.Objects
///
public abstract class StrongNestedHitObject : TaikoHitObject
{
+ public readonly TaikoHitObject Parent;
+
+ protected StrongNestedHitObject(TaikoHitObject parent)
+ {
+ Parent = parent;
+ }
+
public override Judgement CreateJudgement() => new TaikoStrongJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
index 4b60ee3ccb..a77e6db6f3 100644
--- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
+++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
@@ -1,23 +1,44 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Scoring
{
- internal partial class TaikoScoreProcessor : ScoreProcessor
+ public partial class TaikoScoreProcessor : ScoreProcessor
{
+ private const double combo_base = 4;
+
public TaikoScoreProcessor()
: base(new TaikoRuleset())
{
}
- protected override double DefaultAccuracyPortion => 0.75;
+ protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
+ {
+ return 250000 * comboProgress
+ + 750000 * Math.Pow(Accuracy.Value, 3.6) * accuracyProgress
+ + bonusPortion;
+ }
- protected override double DefaultComboPortion => 0.25;
+ protected override double GetBonusScoreChange(JudgementResult result) => base.GetBonusScoreChange(result) * strongScaleValue(result);
- protected override double ClassicScoreMultiplier => 22;
+ protected override double GetComboScoreChange(JudgementResult result)
+ {
+ return Judgement.ToNumericResult(result.Type)
+ * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base))
+ * strongScaleValue(result);
+ }
+
+ private double strongScaleValue(JudgementResult result)
+ {
+ if (result.HitObject is StrongNestedHitObject strong)
+ return strong.Parent is DrumRollTick ? 3 : 7;
+
+ return 1;
+ }
}
}
diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
index 90c7688443..a261185473 100644
--- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
@@ -76,22 +76,38 @@ namespace osu.Game.Tests.Gameplay
// Reset with a miss instead.
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame
{
- Header = new FrameHeader(0, 0, 0, new Dictionary { { HitResult.Miss, 1 } }, DateTimeOffset.Now)
+ Header = new FrameHeader(0, 0, 0, 0, new Dictionary { { HitResult.Miss, 1 } }, new ScoreProcessorStatistics
+ {
+ MaximumBaseScore = 300,
+ BaseScore = 0,
+ AccuracyJudgementCount = 1,
+ ComboPortion = 0,
+ BonusPortion = 0
+ }, DateTimeOffset.Now)
});
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
+ Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(0));
// Reset with no judged hit.
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame
{
- Header = new FrameHeader(0, 0, 0, new Dictionary(), DateTimeOffset.Now)
+ Header = new FrameHeader(0, 0, 0, 0, new Dictionary(), new ScoreProcessorStatistics
+ {
+ MaximumBaseScore = 0,
+ BaseScore = 0,
+ AccuracyJudgementCount = 0,
+ ComboPortion = 0,
+ BonusPortion = 0
+ }, DateTimeOffset.Now)
});
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
Assert.That(scoreProcessor.JudgedHits, Is.Zero);
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
+ Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1));
}
[Test]
diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs
index a2d81c0a75..a77dc8d49b 100644
--- a/osu.Game.Tests/Resources/TestResources.cs
+++ b/osu.Game.Tests/Resources/TestResources.cs
@@ -179,7 +179,7 @@ namespace osu.Game.Tests.Resources
BeatmapHash = beatmap.Hash,
Ruleset = beatmap.Ruleset,
Mods = new Mod[] { new TestModHardRock(), new TestModDoubleTime() },
- TotalScore = 2845370,
+ TotalScore = 284537,
Accuracy = 0.95,
MaxCombo = 999,
Position = 1,
diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
index 826c610f56..e5e96d2033 100644
--- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
+++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
@@ -14,11 +14,12 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
-using osu.Game.Scoring;
+using osu.Game.Scoring.Legacy;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Rulesets.Scoring
@@ -31,7 +32,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
[SetUp]
public void SetUp()
{
- scoreProcessor = new ScoreProcessor(new TestRuleset());
+ scoreProcessor = new ScoreProcessor(new OsuRuleset());
beatmap = new TestBeatmap(new RulesetInfo())
{
HitObjects = new List
@@ -41,15 +42,14 @@ namespace osu.Game.Tests.Rulesets.Scoring
};
}
- [TestCase(ScoringMode.Standardised, HitResult.Meh, 750_000)]
- [TestCase(ScoringMode.Standardised, HitResult.Ok, 800_000)]
+ [TestCase(ScoringMode.Standardised, HitResult.Meh, 116_667)]
+ [TestCase(ScoringMode.Standardised, HitResult.Ok, 233_338)]
[TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)]
- [TestCase(ScoringMode.Classic, HitResult.Meh, 20)]
- [TestCase(ScoringMode.Classic, HitResult.Ok, 23)]
+ [TestCase(ScoringMode.Classic, HitResult.Meh, 0)]
+ [TestCase(ScoringMode.Classic, HitResult.Ok, 2)]
[TestCase(ScoringMode.Classic, HitResult.Great, 36)]
public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
{
- scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(beatmap);
var judgementResult = new JudgementResult(beatmap.HitObjects.Single(), new OsuJudgement())
@@ -58,7 +58,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
};
scoreProcessor.ApplyResult(judgementResult);
- Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(expectedScore).Within(0.5d));
+ Assert.That(scoreProcessor.GetDisplayScore(scoringMode), Is.EqualTo(expectedScore).Within(0.5d));
}
///
@@ -70,39 +70,29 @@ namespace osu.Game.Tests.Rulesets.Scoring
/// Expected score after all objects have been judged, rounded to the nearest integer.
///
/// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and 50% max combo.
- ///
- /// For standardised scoring, is calculated using the following formula:
- /// 1_000_000 * (((3 * ) / (4 * )) * 30% + (bestCombo / maxCombo) * 70%)
- ///
- ///
- /// For classic scoring, is calculated using the following formula:
- /// / * 936
- /// where 936 is simplified from:
- /// 75% * 4 * 300 * (1 + 1/25)
- ///
///
- [TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] // (3 * 0) / (4 * 300) * 300_000 + (0 / 4) * 700_000
- [TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 387_500)] // (3 * 50) / (4 * 300) * 300_000 + (2 / 4) * 700_000
- [TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 425_000)] // (3 * 100) / (4 * 300) * 300_000 + (2 / 4) * 700_000
- [TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 492_857)] // (3 * 200) / (4 * 350) * 300_000 + (2 / 4) * 700_000
- [TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 575_000)] // (3 * 300) / (4 * 300) * 300_000 + (2 / 4) * 700_000
- [TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 575_000)] // (3 * 350) / (4 * 350) * 300_000 + (2 / 4) * 700_000
- [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 700_000)] // (3 * 0) / (4 * 10) * 300_000 + 700_000 (max combo 0)
- [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 925_000)] // (3 * 10) / (4 * 10) * 300_000 + 700_000 (max combo 0)
- [TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (3 * 0) / (4 * 30) * 300_000 + (0 / 4) * 700_000
- [TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] // (3 * 30) / (4 * 30) * 300_000 + (0 / 4) * 700_000
- [TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)] // 1 * 300_000 + 700_000 (max combo 0) + 3 * 10 (bonus points)
- [TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)] // 1 * 300_000 + 700_000 (max combo 0) + 3 * 50 (bonus points)
+ [TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)]
+ [TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 79_333)]
+ [TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 158_667)]
+ [TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 302_402)]
+ [TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 492_894)]
+ [TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 492_894)]
+ [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)]
+ [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 541_894)]
+ [TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
+ [TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 492_894)]
+ [TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)]
+ [TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)]
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)]
- [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 86)]
- [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 104)]
- [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 140)]
- [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 190)]
- [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 190)]
- [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 18)]
- [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 31)]
+ [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 4)]
+ [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 15)]
+ [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 53)]
+ [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 140)]
+ [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 140)]
+ [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)]
+ [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 11)]
[TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
- [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 12)]
+ [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 9)]
[TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 36)]
[TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 36)]
public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore)
@@ -113,59 +103,18 @@ namespace osu.Game.Tests.Rulesets.Scoring
{
HitObjects = new List(Enumerable.Repeat(new TestHitObject(maxResult), 4))
};
- scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(fourObjectBeatmap);
for (int i = 0; i < 4; i++)
{
- var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], new Judgement())
+ var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], new TestJudgement(maxResult))
{
Type = i == 2 ? minResult : hitResult
};
scoreProcessor.ApplyResult(judgementResult);
}
- Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(expectedScore).Within(0.5d));
- }
-
- ///
- /// This test uses a beatmap with four small ticks and one object with the of .
- /// Its goal is to ensure that with the of ,
- /// small ticks contribute to the accuracy portion, but not the combo portion.
- /// In contrast, does not have separate combo and accuracy portion (they are multiplied by each other).
- ///
- [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, 978_571)] // (3 * 10 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
- [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, 914_286)] // (3 * 0 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
- [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 34)]
- [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 30)]
- public void TestSmallTicksAccuracy(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
- {
- IEnumerable hitObjects = Enumerable
- .Repeat(new TestHitObject(HitResult.SmallTickHit), 4)
- .Append(new TestHitObject(HitResult.Ok));
- IBeatmap fiveObjectBeatmap = new TestBeatmap(new RulesetInfo())
- {
- HitObjects = hitObjects.ToList()
- };
- scoreProcessor.Mode.Value = scoringMode;
- scoreProcessor.ApplyBeatmap(fiveObjectBeatmap);
-
- for (int i = 0; i < 4; i++)
- {
- var judgementResult = new JudgementResult(fiveObjectBeatmap.HitObjects[i], new Judgement())
- {
- Type = i == 2 ? HitResult.SmallTickMiss : hitResult
- };
- scoreProcessor.ApplyResult(judgementResult);
- }
-
- var lastJudgementResult = new JudgementResult(fiveObjectBeatmap.HitObjects.Last(), new Judgement())
- {
- Type = HitResult.Ok
- };
- scoreProcessor.ApplyResult(lastJudgementResult);
-
- Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(expectedScore).Within(0.5d));
+ Assert.That(scoreProcessor.GetDisplayScore(scoringMode), Is.EqualTo(expectedScore).Within(0.5d));
}
[Test]
@@ -173,10 +122,9 @@ namespace osu.Game.Tests.Rulesets.Scoring
[Values(ScoringMode.Standardised, ScoringMode.Classic)]
ScoringMode scoringMode)
{
- scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(new TestBeatmap(new RulesetInfo()));
- Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
+ Assert.That(scoreProcessor.GetDisplayScore(scoringMode), Is.Zero);
}
[TestCase(HitResult.IgnoreHit, HitResult.IgnoreMiss)]
@@ -294,28 +242,6 @@ namespace osu.Game.Tests.Rulesets.Scoring
Assert.AreEqual(expectedReturnValue, hitResult.IsScorable());
}
- [TestCase(HitResult.Perfect, 1_000_000)]
- [TestCase(HitResult.SmallTickHit, 1_000_000)]
- [TestCase(HitResult.LargeTickHit, 1_000_000)]
- [TestCase(HitResult.SmallBonus, 1_000_000 + Judgement.SMALL_BONUS_SCORE)]
- [TestCase(HitResult.LargeBonus, 1_000_000 + Judgement.LARGE_BONUS_SCORE)]
- public void TestGetScoreWithExternalStatistics(HitResult result, int expectedScore)
- {
- var statistic = new Dictionary { { result, 1 } };
-
- scoreProcessor.ApplyBeatmap(new Beatmap
- {
- HitObjects = { new TestHitObject(result) }
- });
-
- Assert.That(scoreProcessor.ComputeScore(ScoringMode.Standardised, new ScoreInfo
- {
- Ruleset = new TestRuleset().RulesetInfo,
- MaxCombo = result.AffectsCombo() ? 1 : 0,
- Statistics = statistic
- }), Is.EqualTo(expectedScore).Within(0.5d));
- }
-
#pragma warning disable CS0618
[Test]
public void TestLegacyComboIncrease()
@@ -330,29 +256,6 @@ namespace osu.Game.Tests.Rulesets.Scoring
Assert.That(HitResult.LegacyComboIncrease.IsHit(), Is.True);
Assert.That(HitResult.LegacyComboIncrease.IsScorable(), Is.True);
Assert.That(HitResultExtensions.ALL_TYPES, Does.Not.Contain(HitResult.LegacyComboIncrease));
-
- // Cannot be used to apply results.
- Assert.Throws(() => scoreProcessor.ApplyBeatmap(new Beatmap
- {
- HitObjects = { new TestHitObject(HitResult.LegacyComboIncrease) }
- }));
-
- ScoreInfo testScore = new ScoreInfo
- {
- MaxCombo = 1,
- Statistics = new Dictionary
- {
- { HitResult.Great, 1 }
- },
- MaximumStatistics = new Dictionary
- {
- { HitResult.Great, 1 },
- { HitResult.LegacyComboIncrease, 1 }
- }
- };
-
- double totalScore = new TestScoreProcessor().ComputeScore(ScoringMode.Standardised, testScore);
- Assert.That(totalScore, Is.EqualTo(750_000)); // 500K from accuracy (100%), and 250K from combo (50%).
}
#pragma warning restore CS0618
@@ -362,36 +265,30 @@ namespace osu.Game.Tests.Rulesets.Scoring
const int count_judgements = 1000;
const int count_misses = 1;
- double actual = new TestScoreProcessor().ComputeAccuracy(new ScoreInfo
+ beatmap = new TestBeatmap(new RulesetInfo())
{
- Statistics = new Dictionary
+ HitObjects = new List(Enumerable.Repeat(new TestHitObject(HitResult.Great), count_judgements))
+ };
+
+ scoreProcessor = new TestScoreProcessor();
+ scoreProcessor.ApplyBeatmap(beatmap);
+
+ for (int i = 0; i < beatmap.HitObjects.Count; i++)
+ {
+ scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[i], new TestJudgement(HitResult.Great))
{
- { HitResult.Great, count_judgements - count_misses },
- { HitResult.Miss, count_misses }
- }
- });
+ Type = i == 0 ? HitResult.Miss : HitResult.Great
+ });
+ }
const double expected = (count_judgements - count_misses) / (double)count_judgements;
+ double actual = scoreProcessor.Accuracy.Value;
Assert.That(actual, Is.Not.EqualTo(0.0));
Assert.That(actual, Is.Not.EqualTo(1.0));
Assert.That(actual, Is.EqualTo(expected).Within(Precision.FLOAT_EPSILON));
}
- private class TestRuleset : Ruleset
- {
- public override IEnumerable GetModsFor(ModType type) => throw new NotImplementedException();
-
- public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new NotImplementedException();
-
- public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException();
-
- public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException();
-
- public override string Description => string.Empty;
- public override string ShortName => string.Empty;
- }
-
private class TestJudgement : Judgement
{
public override HitResult MaxResult { get; }
@@ -419,14 +316,18 @@ namespace osu.Game.Tests.Rulesets.Scoring
private partial class TestScoreProcessor : ScoreProcessor
{
- protected override double DefaultAccuracyPortion => 0.5;
- protected override double DefaultComboPortion => 0.5;
-
public TestScoreProcessor()
: base(new TestRuleset())
{
}
+ protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
+ {
+ return 500000 * comboProgress +
+ 500000 * Accuracy.Value * accuracyProgress +
+ bonusPortion;
+ }
+
// ReSharper disable once MemberHidesStaticFromOuterClass
private class TestRuleset : Ruleset
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index ae46dda750..f97019e466 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -31,8 +31,8 @@ namespace osu.Game.Tests.Visual.Gameplay
private HUDOverlay hudOverlay = null!;
- [Cached]
- private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
+ [Cached(typeof(ScoreProcessor))]
+ private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor;
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs
index 8fff07e6d8..2b378c8013 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs
@@ -22,6 +22,7 @@ using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring.Legacy;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
@@ -124,8 +125,8 @@ namespace osu.Game.Tests.Visual.Gameplay
graphs.Clear();
legend.Clear();
- runForProcessor("lazer-standardised", Color4.YellowGreen, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Standardised } });
- runForProcessor("lazer-classic", Color4.MediumPurple, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Classic } });
+ runForProcessor("lazer-standardised", Color4.YellowGreen, new ScoreProcessor(new OsuRuleset()), ScoringMode.Standardised);
+ runForProcessor("lazer-classic", Color4.MediumPurple, new ScoreProcessor(new OsuRuleset()), ScoringMode.Classic);
runScoreV1();
runScoreV2();
@@ -218,7 +219,7 @@ namespace osu.Game.Tests.Visual.Gameplay
});
}
- private void runForProcessor(string name, Color4 colour, ScoreProcessor processor)
+ private void runForProcessor(string name, Color4 colour, ScoreProcessor processor, ScoringMode mode)
{
int maxCombo = sliderMaxCombo.Current.Value;
@@ -232,10 +233,10 @@ namespace osu.Game.Tests.Visual.Gameplay
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }),
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }),
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }),
- () => (int)processor.TotalScore.Value);
+ () => processor.GetDisplayScore(mode));
}
- private void runForAlgorithm(string name, Color4 colour, Action applyHit, Action applyNonPerfect, Action applyMiss, Func getTotalScore)
+ private void runForAlgorithm(string name, Color4 colour, Action applyHit, Action applyNonPerfect, Action applyMiss, Func getTotalScore)
{
int maxCombo = sliderMaxCombo.Current.Value;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs
index 93fec60de4..4ae115a68d 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs
@@ -22,8 +22,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneSkinEditorMultipleSkins : SkinnableTestScene
{
- [Cached]
- private readonly ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
+ [Cached(typeof(ScoreProcessor))]
+ private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor;
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
index 0439656aae..89432940ba 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
@@ -28,8 +28,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
private HUDOverlay hudOverlay;
- [Cached]
- private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
+ [Cached(typeof(ScoreProcessor))]
+ private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor;
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs
index c95e8ee5b2..2cb3303dd6 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs
@@ -10,13 +10,14 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
+using osu.Game.Tests.Gameplay;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneSkinnableScoreCounter : SkinnableHUDComponentTestScene
{
- [Cached]
- private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
+ [Cached(typeof(ScoreProcessor))]
+ private ScoreProcessor scoreProcessor = TestGameplayState.Create(new OsuRuleset()).ScoreProcessor;
protected override Drawable CreateDefaultImplementation() => new DefaultScoreCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyScoreCounter();
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs
index 8ae6a2a5fc..dbd14db818 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs
@@ -16,13 +16,14 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Select;
+using osu.Game.Tests.Gameplay;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneSoloGameplayLeaderboard : OsuTestScene
{
- [Cached]
- private readonly ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
+ [Cached(typeof(ScoreProcessor))]
+ private readonly ScoreProcessor scoreProcessor = TestGameplayState.Create(new OsuRuleset()).ScoreProcessor;
private readonly BindableList scores = new BindableList();
diff --git a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs
index 649c662e41..906eea9553 100644
--- a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs
@@ -21,7 +21,6 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Multiplayer
@@ -188,15 +187,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
if (!lastHeaders.TryGetValue(userId, out var header))
{
- lastHeaders[userId] = header = new FrameHeader(new ScoreInfo
+ lastHeaders[userId] = header = new FrameHeader(0, 0, 0, 0, new Dictionary
{
- Statistics = new Dictionary
- {
- [HitResult.Miss] = 0,
- [HitResult.Meh] = 0,
- [HitResult.Great] = 0
- }
- });
+ [HitResult.Miss] = 0,
+ [HitResult.Meh] = 0,
+ [HitResult.Great] = 0
+ }, new ScoreProcessorStatistics(), DateTimeOffset.Now);
}
switch (RNG.Next(0, 3))
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs
index cf0de14541..79baae53e8 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -12,7 +11,6 @@ using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Select.Carousel;
using osu.Game.Tests.Resources;
@@ -143,25 +141,20 @@ namespace osu.Game.Tests.Visual.SongSelect
testScoreInfo.User = API.LocalUser.Value;
testScoreInfo.Rank = ScoreRank.B;
- testScoreInfo.TotalScore = scoreManager.GetTotalScore(testScoreInfo, ScoringMode.Classic);
scoreManager.Import(testScoreInfo);
});
AddUntilStep("B rank displayed", () => topLocalRank.DisplayedRank == ScoreRank.B);
- AddStep("Add higher score for current user", () =>
+ AddStep("Add higher-graded score for current user", () =>
{
var testScoreInfo2 = TestResources.CreateTestScoreInfo(importedBeatmap);
testScoreInfo2.User = API.LocalUser.Value;
testScoreInfo2.Rank = ScoreRank.X;
testScoreInfo2.Statistics = testScoreInfo2.MaximumStatistics;
- testScoreInfo2.TotalScore = scoreManager.GetTotalScore(testScoreInfo2);
-
- // ensure second score has a total score (standardised) less than first one (classic)
- // despite having better statistics, otherwise this test is pointless.
- Debug.Assert(testScoreInfo2.TotalScore < testScoreInfo.TotalScore);
+ testScoreInfo2.TotalScore = testScoreInfo.TotalScore + 1;
scoreManager.Import(testScoreInfo2);
});
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index 831e328439..94108531e8 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -179,43 +179,9 @@ namespace osu.Game.Database
applyFilenameSchemaSuffix(ref Filename);
#endif
- string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}";
-
- // Attempt to recover a newer database version if available.
- if (storage.Exists(newerVersionFilename))
- {
- Logger.Log(@"A newer realm database has been found, attempting recovery...", LoggingTarget.Database);
- attemptRecoverFromFile(newerVersionFilename);
- }
-
- try
- {
- // This method triggers the first `getRealmInstance` call, which will implicitly run realm migrations and bring the schema up-to-date.
- cleanupPendingDeletions();
- }
- catch (Exception e)
- {
- // See https://github.com/realm/realm-core/blob/master/src%2Frealm%2Fobject-store%2Fobject_store.cpp#L1016-L1022
- // This is the best way we can detect a schema version downgrade.
- if (e.Message.StartsWith(@"Provided schema version", StringComparison.Ordinal))
- {
- Logger.Error(e, "Your local database is too new to work with this version of osu!. Please close osu! and install the latest release to recover your data.");
-
- // If a newer version database already exists, don't backup again. We can presume that the first backup is the one we care about.
- if (!storage.Exists(newerVersionFilename))
- createBackup(newerVersionFilename);
-
- storage.Delete(Filename);
- }
- else
- {
- Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made.");
- createBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}");
- storage.Delete(Filename);
- }
-
- cleanupPendingDeletions();
- }
+ // `prepareFirstRealmAccess()` triggers the first `getRealmInstance` call, which will implicitly run realm migrations and bring the schema up-to-date.
+ using (var realm = prepareFirstRealmAccess())
+ cleanupPendingDeletions(realm);
}
///
@@ -312,49 +278,93 @@ namespace osu.Game.Database
Logger.Log(@"Recovery complete!", LoggingTarget.Database);
}
- private void cleanupPendingDeletions()
+ private Realm prepareFirstRealmAccess()
{
- using (var realm = getRealmInstance())
- using (var transaction = realm.BeginWrite())
+ string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}";
+
+ // Attempt to recover a newer database version if available.
+ if (storage.Exists(newerVersionFilename))
{
- var pendingDeleteScores = realm.All().Where(s => s.DeletePending);
-
- foreach (var score in pendingDeleteScores)
- realm.Remove(score);
-
- var pendingDeleteSets = realm.All().Where(s => s.DeletePending);
-
- foreach (var beatmapSet in pendingDeleteSets)
- {
- foreach (var beatmap in beatmapSet.Beatmaps)
- {
- // Cascade delete related scores, else they will have a null beatmap against the model's spec.
- foreach (var score in beatmap.Scores)
- realm.Remove(score);
-
- realm.Remove(beatmap.Metadata);
- realm.Remove(beatmap);
- }
-
- realm.Remove(beatmapSet);
- }
-
- var pendingDeleteSkins = realm.All().Where(s => s.DeletePending);
-
- foreach (var s in pendingDeleteSkins)
- realm.Remove(s);
-
- var pendingDeletePresets = realm.All().Where(s => s.DeletePending);
-
- foreach (var s in pendingDeletePresets)
- realm.Remove(s);
-
- transaction.Commit();
+ Logger.Log(@"A newer realm database has been found, attempting recovery...", LoggingTarget.Database);
+ attemptRecoverFromFile(newerVersionFilename);
}
- // clean up files after dropping any pending deletions.
- // in the future we may want to only do this when the game is idle, rather than on every startup.
- new RealmFileStore(this, storage).Cleanup();
+ try
+ {
+ return getRealmInstance();
+ }
+ catch (Exception e)
+ {
+ // See https://github.com/realm/realm-core/blob/master/src%2Frealm%2Fobject-store%2Fobject_store.cpp#L1016-L1022
+ // This is the best way we can detect a schema version downgrade.
+ if (e.Message.StartsWith(@"Provided schema version", StringComparison.Ordinal))
+ {
+ Logger.Error(e, "Your local database is too new to work with this version of osu!. Please close osu! and install the latest release to recover your data.");
+
+ // If a newer version database already exists, don't backup again. We can presume that the first backup is the one we care about.
+ if (!storage.Exists(newerVersionFilename))
+ createBackup(newerVersionFilename);
+ }
+ else
+ {
+ Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made.");
+ createBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}");
+ }
+
+ storage.Delete(Filename);
+ return getRealmInstance();
+ }
+ }
+
+ private void cleanupPendingDeletions(Realm realm)
+ {
+ try
+ {
+ using (var transaction = realm.BeginWrite())
+ {
+ var pendingDeleteScores = realm.All().Where(s => s.DeletePending);
+
+ foreach (var score in pendingDeleteScores)
+ realm.Remove(score);
+
+ var pendingDeleteSets = realm.All().Where(s => s.DeletePending);
+
+ foreach (var beatmapSet in pendingDeleteSets)
+ {
+ foreach (var beatmap in beatmapSet.Beatmaps)
+ {
+ // Cascade delete related scores, else they will have a null beatmap against the model's spec.
+ foreach (var score in beatmap.Scores)
+ realm.Remove(score);
+
+ realm.Remove(beatmap.Metadata);
+ realm.Remove(beatmap);
+ }
+
+ realm.Remove(beatmapSet);
+ }
+
+ var pendingDeleteSkins = realm.All().Where(s => s.DeletePending);
+
+ foreach (var s in pendingDeleteSkins)
+ realm.Remove(s);
+
+ var pendingDeletePresets = realm.All().Where(s => s.DeletePending);
+
+ foreach (var s in pendingDeletePresets)
+ realm.Remove(s);
+
+ transaction.Commit();
+ }
+
+ // clean up files after dropping any pending deletions.
+ // in the future we may want to only do this when the game is idle, rather than on every startup.
+ new RealmFileStore(this, storage).Cleanup();
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, "Failed to clean up unused files. This is not critical but please report if it happens regularly.");
+ }
}
///
@@ -909,7 +919,7 @@ namespace osu.Game.Database
int attempts = 10;
- while (attempts-- > 0)
+ while (true)
{
try
{
@@ -927,6 +937,9 @@ namespace osu.Game.Database
}
catch (IOException)
{
+ if (attempts-- <= 0)
+ throw;
+
// file may be locked during use.
Thread.Sleep(500);
}
diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs
index d799e82bc9..82f89d6889 100644
--- a/osu.Game/Graphics/ScreenshotManager.cs
+++ b/osu.Game/Graphics/ScreenshotManager.cs
@@ -19,6 +19,7 @@ using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Configuration;
using osu.Game.Input.Bindings;
+using osu.Game.Online.Multiplayer;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using SixLabors.ImageSharp;
@@ -69,7 +70,7 @@ namespace osu.Game.Graphics
{
case GlobalAction.TakeScreenshot:
shutter.Play();
- TakeScreenshotAsync();
+ TakeScreenshotAsync().FireAndForget();
return true;
}
@@ -86,70 +87,75 @@ namespace osu.Game.Graphics
{
Interlocked.Increment(ref screenShotTasks);
- if (!captureMenuCursor.Value)
+ try
{
- cursorVisibility.Value = false;
-
- // We need to wait for at most 3 draw nodes to be drawn, following which we can be assured at least one DrawNode has been generated/drawn with the set value
- const int frames_to_wait = 3;
-
- int framesWaited = 0;
-
- using (var framesWaitedEvent = new ManualResetEventSlim(false))
+ if (!captureMenuCursor.Value)
{
- ScheduledDelegate waitDelegate = host.DrawThread.Scheduler.AddDelayed(() =>
+ cursorVisibility.Value = false;
+
+ // We need to wait for at most 3 draw nodes to be drawn, following which we can be assured at least one DrawNode has been generated/drawn with the set value
+ const int frames_to_wait = 3;
+
+ int framesWaited = 0;
+
+ using (var framesWaitedEvent = new ManualResetEventSlim(false))
{
- if (framesWaited++ >= frames_to_wait)
- // ReSharper disable once AccessToDisposedClosure
- framesWaitedEvent.Set();
- }, 10, true);
+ ScheduledDelegate waitDelegate = host.DrawThread.Scheduler.AddDelayed(() =>
+ {
+ if (framesWaited++ >= frames_to_wait)
+ // ReSharper disable once AccessToDisposedClosure
+ framesWaitedEvent.Set();
+ }, 10, true);
- if (!framesWaitedEvent.Wait(1000))
- throw new TimeoutException("Screenshot data did not arrive in a timely fashion");
+ if (!framesWaitedEvent.Wait(1000))
+ throw new TimeoutException("Screenshot data did not arrive in a timely fashion");
- waitDelegate.Cancel();
+ waitDelegate.Cancel();
+ }
+ }
+
+ using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false))
+ {
+ host.GetClipboard()?.SetImage(image);
+
+ (string filename, var stream) = getWritableStream();
+
+ if (filename == null) return;
+
+ using (stream)
+ {
+ switch (screenshotFormat.Value)
+ {
+ case ScreenshotFormat.Png:
+ await image.SaveAsPngAsync(stream).ConfigureAwait(false);
+ break;
+
+ case ScreenshotFormat.Jpg:
+ const int jpeg_quality = 92;
+
+ await image.SaveAsJpegAsync(stream, new JpegEncoder { Quality = jpeg_quality }).ConfigureAwait(false);
+ break;
+
+ default:
+ throw new InvalidOperationException($"Unknown enum member {nameof(ScreenshotFormat)} {screenshotFormat.Value}.");
+ }
+ }
+
+ notificationOverlay.Post(new SimpleNotification
+ {
+ Text = $"Screenshot {filename} saved!",
+ Activated = () =>
+ {
+ storage.PresentFileExternally(filename);
+ return true;
+ }
+ });
}
}
-
- using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false))
+ finally
{
- if (Interlocked.Decrement(ref screenShotTasks) == 0 && cursorVisibility.Value == false)
+ if (Interlocked.Decrement(ref screenShotTasks) == 0)
cursorVisibility.Value = true;
-
- host.GetClipboard()?.SetImage(image);
-
- (string filename, var stream) = getWritableStream();
-
- if (filename == null) return;
-
- using (stream)
- {
- switch (screenshotFormat.Value)
- {
- case ScreenshotFormat.Png:
- await image.SaveAsPngAsync(stream).ConfigureAwait(false);
- break;
-
- case ScreenshotFormat.Jpg:
- const int jpeg_quality = 92;
-
- await image.SaveAsJpegAsync(stream, new JpegEncoder { Quality = jpeg_quality }).ConfigureAwait(false);
- break;
-
- default:
- throw new InvalidOperationException($"Unknown enum member {nameof(ScreenshotFormat)} {screenshotFormat.Value}.");
- }
- }
-
- notificationOverlay.Post(new SimpleNotification
- {
- Text = $"Screenshot {filename} saved!",
- Activated = () =>
- {
- storage.PresentFileExternally(filename);
- return true;
- }
- });
}
});
diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
index 9b7087ce6d..16bad5785f 100644
--- a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs
@@ -152,7 +152,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
[BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider? colourProvider, OsuColour osuColour)
{
- background.Colour = colourProvider?.Background5 ?? Color4Extensions.FromHex(@"1c2125");
+ background.Colour = colourProvider?.Background4 ?? Color4Extensions.FromHex(@"1c2125");
descriptionText.Colour = osuColour.Yellow;
}
diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs
index 52e6a5eaac..91bbece004 100644
--- a/osu.Game/Localisation/RulesetSettingsStrings.cs
+++ b/osu.Game/Localisation/RulesetSettingsStrings.cs
@@ -82,7 +82,7 @@ namespace osu.Game.Localisation
///
/// "{0}ms (speed {1})"
///
- public static LocalisableString ScrollSpeedTooltip(double arg0, int arg1) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1})", arg0, arg1);
+ public static LocalisableString ScrollSpeedTooltip(double scrollTime, int scrollSpeed) => new TranslatableString(getKey(@"ruleset"), @"{0:0}ms (speed {1})", scrollTime, scrollSpeed);
private static string getKey(string key) => $@"{prefix}:{key}";
}
diff --git a/osu.Game/Online/Spectator/FrameDataBundle.cs b/osu.Game/Online/Spectator/FrameDataBundle.cs
index 97ae468875..d58ddd5310 100644
--- a/osu.Game/Online/Spectator/FrameDataBundle.cs
+++ b/osu.Game/Online/Spectator/FrameDataBundle.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using MessagePack;
using Newtonsoft.Json;
using osu.Game.Replays.Legacy;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Online.Spectator
@@ -20,10 +21,10 @@ namespace osu.Game.Online.Spectator
[Key(1)]
public IList Frames { get; set; }
- public FrameDataBundle(ScoreInfo score, IList frames)
+ public FrameDataBundle(ScoreInfo score, ScoreProcessor scoreProcessor, IList frames)
{
Frames = frames;
- Header = new FrameHeader(score);
+ Header = new FrameHeader(score, scoreProcessor.GetScoreProcessorStatistics());
}
[JsonConstructor]
diff --git a/osu.Game/Online/Spectator/FrameHeader.cs b/osu.Game/Online/Spectator/FrameHeader.cs
index b6dcd8aaa5..45f920e65b 100644
--- a/osu.Game/Online/Spectator/FrameHeader.cs
+++ b/osu.Game/Online/Spectator/FrameHeader.cs
@@ -15,57 +15,74 @@ namespace osu.Game.Online.Spectator
public class FrameHeader
{
///
- /// The current accuracy of the score.
+ /// The total score.
///
[Key(0)]
+ public long TotalScore { get; set; }
+
+ ///
+ /// The current accuracy of the score.
+ ///
+ [Key(1)]
public double Accuracy { get; set; }
///
/// The current combo of the score.
///
- [Key(1)]
+ [Key(2)]
public int Combo { get; set; }
///
/// The maximum combo achieved up to the current point in time.
///
- [Key(2)]
+ [Key(3)]
public int MaxCombo { get; set; }
///
/// Cumulative hit statistics.
///
- [Key(3)]
+ [Key(4)]
public Dictionary Statistics { get; set; }
+ ///
+ /// Additional statistics that guides the score processor to calculate the correct score for this frame.
+ ///
+ [Key(5)]
+ public ScoreProcessorStatistics ScoreProcessorStatistics { get; set; }
+
///
/// The time at which this frame was received by the server.
///
- [Key(4)]
+ [Key(6)]
public DateTimeOffset ReceivedTime { get; set; }
///
/// Construct header summary information from a point-in-time reference to a score which is actively being played.
///
/// The score for reference.
- public FrameHeader(ScoreInfo score)
+ /// The score processor statistics for the current point in time.
+ public FrameHeader(ScoreInfo score, ScoreProcessorStatistics statistics)
{
+ TotalScore = score.TotalScore;
+ Accuracy = score.Accuracy;
Combo = score.Combo;
MaxCombo = score.MaxCombo;
- Accuracy = score.Accuracy;
-
// copy for safety
Statistics = new Dictionary(score.Statistics);
+
+ ScoreProcessorStatistics = statistics;
}
[JsonConstructor]
[SerializationConstructor]
- public FrameHeader(double accuracy, int combo, int maxCombo, Dictionary statistics, DateTimeOffset receivedTime)
+ public FrameHeader(long totalScore, double accuracy, int combo, int maxCombo, Dictionary statistics, ScoreProcessorStatistics scoreProcessorStatistics, DateTimeOffset receivedTime)
{
+ TotalScore = totalScore;
+ Accuracy = accuracy;
Combo = combo;
MaxCombo = maxCombo;
- Accuracy = accuracy;
Statistics = statistics;
+ ScoreProcessorStatistics = scoreProcessorStatistics;
ReceivedTime = receivedTime;
}
}
diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs
index 55ec75f4ce..89da8b9d32 100644
--- a/osu.Game/Online/Spectator/SpectatorClient.cs
+++ b/osu.Game/Online/Spectator/SpectatorClient.cs
@@ -16,6 +16,7 @@ using osu.Game.Online.API;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
@@ -82,6 +83,7 @@ namespace osu.Game.Online.Spectator
private IBeatmap? currentBeatmap;
private Score? currentScore;
private long? currentScoreToken;
+ private ScoreProcessor? currentScoreProcessor;
private readonly Queue pendingFrameBundles = new Queue();
@@ -192,6 +194,7 @@ namespace osu.Game.Online.Spectator
currentBeatmap = state.Beatmap;
currentScore = score;
currentScoreToken = scoreToken;
+ currentScoreProcessor = state.ScoreProcessor;
BeginPlayingInternal(currentScoreToken, currentState);
});
@@ -302,9 +305,10 @@ namespace osu.Game.Online.Spectator
return;
Debug.Assert(currentScore != null);
+ Debug.Assert(currentScoreProcessor != null);
var frames = pendingFrames.ToArray();
- var bundle = new FrameDataBundle(currentScore.ScoreInfo, frames);
+ var bundle = new FrameDataBundle(currentScore.ScoreInfo, currentScoreProcessor, frames);
pendingFrames.Clear();
lastPurgeTime = Time.Current;
diff --git a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs
index 1c505ea107..3242e21994 100644
--- a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs
+++ b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs
@@ -14,6 +14,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
+using osu.Game.Scoring.Legacy;
namespace osu.Game.Online.Spectator
{
@@ -46,7 +47,9 @@ namespace osu.Game.Online.Spectator
///
/// The applied s.
///
- public IReadOnlyList Mods => scoreProcessor?.Mods.Value ?? Array.Empty();
+ public IReadOnlyList Mods => scoreInfo?.Mods ?? Array.Empty();
+
+ public Func GetDisplayScore => mode => scoreInfo?.GetDisplayScore(mode) ?? 0;
private IClock? referenceClock;
@@ -70,7 +73,6 @@ namespace osu.Game.Online.Spectator
private readonly int userId;
private SpectatorState? spectatorState;
- private ScoreProcessor? scoreProcessor;
private ScoreInfo? scoreInfo;
public SpectatorScoreProcessor(int userId)
@@ -94,19 +96,15 @@ namespace osu.Game.Online.Spectator
{
if (!spectatorStates.TryGetValue(userId, out var userState) || userState.BeatmapID == null || userState.RulesetID == null)
{
- scoreProcessor?.RemoveAndDisposeImmediately();
- scoreProcessor = null;
scoreInfo = null;
spectatorState = null;
replayFrames.Clear();
return;
}
- if (scoreProcessor != null)
+ if (scoreInfo != null)
return;
- Debug.Assert(scoreInfo == null);
-
RulesetInfo? rulesetInfo = rulesetStore.GetRuleset(userState.RulesetID.Value);
if (rulesetInfo == null)
return;
@@ -114,9 +112,11 @@ namespace osu.Game.Online.Spectator
Ruleset ruleset = rulesetInfo.CreateInstance();
spectatorState = userState;
- scoreInfo = new ScoreInfo { Ruleset = rulesetInfo };
- scoreProcessor = ruleset.CreateScoreProcessor();
- scoreProcessor.Mods.Value = userState.Mods.Select(m => m.ToMod(ruleset)).ToArray();
+ scoreInfo = new ScoreInfo
+ {
+ Ruleset = rulesetInfo,
+ Mods = userState.Mods.Select(m => m.ToMod(ruleset)).ToArray()
+ };
}
private void onNewFrames(int incomingUserId, FrameDataBundle bundle)
@@ -126,7 +126,7 @@ namespace osu.Game.Online.Spectator
Schedule(() =>
{
- if (scoreProcessor == null)
+ if (scoreInfo == null)
return;
replayFrames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header));
@@ -140,7 +140,6 @@ namespace osu.Game.Online.Spectator
return;
Debug.Assert(spectatorState != null);
- Debug.Assert(scoreProcessor != null);
int frameIndex = replayFrames.BinarySearch(new TimedFrame(ReferenceClock.CurrentTime));
if (frameIndex < 0)
@@ -150,14 +149,15 @@ namespace osu.Game.Online.Spectator
TimedFrame frame = replayFrames[frameIndex];
Debug.Assert(frame.Header != null);
+ scoreInfo.Accuracy = frame.Header.Accuracy;
scoreInfo.MaxCombo = frame.Header.MaxCombo;
scoreInfo.Statistics = frame.Header.Statistics;
scoreInfo.MaximumStatistics = spectatorState.MaximumStatistics;
+ scoreInfo.TotalScore = frame.Header.TotalScore;
Accuracy.Value = frame.Header.Accuracy;
Combo.Value = frame.Header.Combo;
-
- TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, scoreInfo);
+ TotalScore.Value = frame.Header.TotalScore;
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs
index 4f802a22a1..64a04f896f 100644
--- a/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/PerformanceBreakdownCalculator.cs
@@ -62,11 +62,13 @@ namespace osu.Game.Rulesets.Difficulty
.GroupBy(hr => hr, (hr, list) => (hitResult: hr, count: list.Count()))
.ToDictionary(pair => pair.hitResult, pair => pair.count);
perfectPlay.Statistics = statistics;
+ perfectPlay.MaximumStatistics = statistics;
// calculate total score
ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = perfectPlay.Mods;
- perfectPlay.TotalScore = scoreProcessor.ComputeScore(ScoringMode.Standardised, perfectPlay);
+ scoreProcessor.ApplyBeatmap(playableBeatmap);
+ perfectPlay.TotalScore = scoreProcessor.MaximumTotalScore;
// compute rank achieved
// default to SS, then adjust the rank with mods
diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs
index bf29919e34..34d1f1f6e9 100644
--- a/osu.Game/Rulesets/Judgements/JudgementResult.cs
+++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs
@@ -64,6 +64,11 @@ namespace osu.Game.Rulesets.Judgements
///
public int ComboAtJudgement { get; internal set; }
+ ///
+ /// The combo after this occurred.
+ ///
+ public int ComboAfterJudgement { get; internal set; }
+
///
/// The highest combo achieved prior to this occurring.
///
diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs
index 83ed98768c..0013a9f20d 100644
--- a/osu.Game/Rulesets/Scoring/HitResult.cs
+++ b/osu.Game/Rulesets/Scoring/HitResult.cs
@@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Scoring
=> AffectsCombo(result) && !IsHit(result);
///
- /// Whether a increases/breaks the combo, and affects the combo portion of the score.
+ /// Whether a increases or breaks the combo.
///
public static bool AffectsCombo(this HitResult result)
{
diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index 96f6922224..ac17de32d8 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -4,11 +4,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Diagnostics.Contracts;
using System.Linq;
+using MessagePack;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
-using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Localisation;
@@ -22,6 +21,8 @@ namespace osu.Game.Rulesets.Scoring
{
public partial class ScoreProcessor : JudgementProcessor
{
+ public const double MAX_SCORE = 1000000;
+
private const double accuracy_cutoff_x = 1;
private const double accuracy_cutoff_s = 0.95;
private const double accuracy_cutoff_a = 0.9;
@@ -29,8 +30,6 @@ namespace osu.Game.Rulesets.Scoring
private const double accuracy_cutoff_c = 0.7;
private const double accuracy_cutoff_d = 0;
- private const double max_score = 1000000;
-
///
/// Invoked when this was reset from a replay frame.
///
@@ -78,39 +77,72 @@ namespace osu.Game.Rulesets.Scoring
///
public readonly BindableInt HighestCombo = new BindableInt();
- ///
- /// The used to calculate scores.
- ///
- public readonly Bindable Mode = new Bindable();
-
///
/// The s collected during gameplay thus far.
/// Intended for use with various statistics displays.
///
public IReadOnlyList HitEvents => hitEvents;
- ///
- /// The default portion of awarded for hitting s accurately. Defaults to 30%.
- ///
- protected virtual double DefaultAccuracyPortion => 0.3;
-
- ///
- /// The default portion of awarded for achieving a high combo. Default to 70%.
- ///
- protected virtual double DefaultComboPortion => 0.7;
-
- ///
- /// An arbitrary multiplier to scale scores in the scoring mode.
- ///
- protected virtual double ClassicScoreMultiplier => 36;
-
///
/// The ruleset this score processor is valid for.
///
public readonly Ruleset Ruleset;
- private readonly double accuracyPortion;
- private readonly double comboPortion;
+ ///
+ /// The maximum achievable total score.
+ ///
+ public long MaximumTotalScore { get; private set; }
+
+ ///
+ /// The maximum sum of accuracy-affecting judgements at the current point in time.
+ ///
+ ///
+ /// Used to compute accuracy.
+ ///
+ private double currentMaximumBaseScore;
+
+ ///
+ /// The sum of all accuracy-affecting judgements at the current point in time.
+ ///
+ ///
+ /// Used to compute accuracy.
+ ///
+ private double currentBaseScore;
+
+ ///
+ /// The maximum sum of all accuracy-affecting judgements in the beatmap.
+ ///
+ private double maximumBaseScore;
+
+ ///
+ /// The count of all accuracy-affecting judgements in the beatmap.
+ ///
+ private int maximumAccuracyJudgementCount;
+
+ ///
+ /// The count of accuracy-affecting judgements at the current point in time.
+ ///
+ private int currentAccuracyJudgementCount;
+
+ ///
+ /// The maximum combo score in the beatmap.
+ ///
+ private double maximumComboPortion;
+
+ ///
+ /// The combo score at the current point in time.
+ ///
+ private double currentComboPortion;
+
+ ///
+ /// The bonus score at the current point in time.
+ ///
+ private double currentBonusPortion;
+
+ ///
+ /// The total score multiplier.
+ ///
+ private double scoreMultiplier = 1;
public Dictionary MaximumStatistics
{
@@ -123,27 +155,6 @@ namespace osu.Game.Rulesets.Scoring
}
}
- private ScoringValues maximumScoringValues;
-
- ///
- /// Scoring values for the current play assuming all perfect hits.
- ///
- ///
- /// This is only used to determine the accuracy with respect to the current point in time for an ongoing play session.
- ///
- private ScoringValues currentMaximumScoringValues;
-
- ///
- /// Scoring values for the current play.
- ///
- private ScoringValues currentScoringValues;
-
- ///
- /// The maximum of a basic (non-tick and non-bonus) hitobject.
- /// Only populated via or .
- ///
- private HitResult? maxBasicResult;
-
private bool beatmapApplied;
private readonly Dictionary scoreResultCounts = new Dictionary();
@@ -152,18 +163,10 @@ namespace osu.Game.Rulesets.Scoring
private readonly List hitEvents = new List();
private HitObject? lastHitObject;
- private double scoreMultiplier = 1;
-
public ScoreProcessor(Ruleset ruleset)
{
Ruleset = ruleset;
- accuracyPortion = DefaultAccuracyPortion;
- comboPortion = DefaultComboPortion;
-
- if (!Precision.AlmostEquals(1.0, accuracyPortion + comboPortion))
- throw new InvalidOperationException($"{nameof(DefaultAccuracyPortion)} + {nameof(DefaultComboPortion)} must equal 1.");
-
Combo.ValueChanged += combo => HighestCombo.Value = Math.Max(HighestCombo.Value, combo.NewValue);
Accuracy.ValueChanged += accuracy =>
{
@@ -172,7 +175,6 @@ namespace osu.Game.Rulesets.Scoring
Rank.Value = mod.AdjustRank(Rank.Value, accuracy.NewValue);
};
- Mode.ValueChanged += _ => updateScore();
Mods.ValueChanged += mods =>
{
scoreMultiplier = 1;
@@ -200,10 +202,6 @@ namespace osu.Game.Rulesets.Scoring
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1;
- // Always update the maximum scoring values.
- applyResult(result.Judgement.MaxResult, ref currentMaximumScoringValues);
- currentMaximumScoringValues.MaxCombo += result.Judgement.MaxResult.IncreasesCombo() ? 1 : 0;
-
if (!result.Type.IsScorable())
return;
@@ -212,8 +210,21 @@ namespace osu.Game.Rulesets.Scoring
else if (result.Type.BreaksCombo())
Combo.Value = 0;
- applyResult(result.Type, ref currentScoringValues);
- currentScoringValues.MaxCombo = HighestCombo.Value;
+ result.ComboAfterJudgement = Combo.Value;
+
+ if (result.Type.AffectsAccuracy())
+ {
+ currentMaximumBaseScore += Judgement.ToNumericResult(result.Judgement.MaxResult);
+ currentBaseScore += Judgement.ToNumericResult(result.Type);
+ currentAccuracyJudgementCount++;
+ }
+
+ if (result.Type.IsBonus())
+ currentBonusPortion += GetBonusScoreChange(result);
+ else
+ currentComboPortion += GetComboScoreChange(result);
+
+ ApplyScoreChange(result);
hitEvents.Add(CreateHitEvent(result));
lastHitObject = result.HitObject;
@@ -221,20 +232,6 @@ namespace osu.Game.Rulesets.Scoring
updateScore();
}
- private static void applyResult(HitResult result, ref ScoringValues scoringValues)
- {
- if (!result.IsScorable())
- return;
-
- if (result.IsBonus())
- scoringValues.BonusScore += result.IsHit() ? Judgement.ToNumericResult(result) : 0;
- else
- scoringValues.BaseScore += result.IsHit() ? Judgement.ToNumericResult(result) : 0;
-
- if (result.IsBasic())
- scoringValues.CountBasicHitObjects++;
- }
-
///
/// Creates the that describes a .
///
@@ -253,15 +250,22 @@ namespace osu.Game.Rulesets.Scoring
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1;
- // Always update the maximum scoring values.
- revertResult(result.Judgement.MaxResult, ref currentMaximumScoringValues);
- currentMaximumScoringValues.MaxCombo -= result.Judgement.MaxResult.IncreasesCombo() ? 1 : 0;
-
if (!result.Type.IsScorable())
return;
- revertResult(result.Type, ref currentScoringValues);
- currentScoringValues.MaxCombo = HighestCombo.Value;
+ if (result.Type.AffectsAccuracy())
+ {
+ currentMaximumBaseScore -= Judgement.ToNumericResult(result.Judgement.MaxResult);
+ currentBaseScore -= Judgement.ToNumericResult(result.Type);
+ currentAccuracyJudgementCount--;
+ }
+
+ if (result.Type.IsBonus())
+ currentBonusPortion -= GetBonusScoreChange(result);
+ else
+ currentComboPortion -= GetComboScoreChange(result);
+
+ RemoveScoreChange(result);
Debug.Assert(hitEvents.Count > 0);
lastHitObject = hitEvents[^1].LastHitObject;
@@ -270,110 +274,35 @@ namespace osu.Game.Rulesets.Scoring
updateScore();
}
- private static void revertResult(HitResult result, ref ScoringValues scoringValues)
+ protected virtual double GetBonusScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Type);
+
+ protected virtual double GetComboScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Type) * (1 + result.ComboAfterJudgement / 10d);
+
+ protected virtual void ApplyScoreChange(JudgementResult result)
{
- if (!result.IsScorable())
- return;
+ }
- if (result.IsBonus())
- scoringValues.BonusScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0;
- else
- scoringValues.BaseScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0;
-
- if (result.IsBasic())
- scoringValues.CountBasicHitObjects--;
+ protected virtual void RemoveScoreChange(JudgementResult result)
+ {
}
private void updateScore()
{
- Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? (double)currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1;
- MinimumAccuracy.Value = maximumScoringValues.BaseScore > 0 ? (double)currentScoringValues.BaseScore / maximumScoringValues.BaseScore : 0;
- MaximumAccuracy.Value = maximumScoringValues.BaseScore > 0
- ? (double)(currentScoringValues.BaseScore + (maximumScoringValues.BaseScore - currentMaximumScoringValues.BaseScore)) / maximumScoringValues.BaseScore
- : 1;
- TotalScore.Value = computeScore(Mode.Value, currentScoringValues, maximumScoringValues);
+ Accuracy.Value = currentMaximumBaseScore > 0 ? currentBaseScore / currentMaximumBaseScore : 1;
+ MinimumAccuracy.Value = maximumBaseScore > 0 ? currentBaseScore / maximumBaseScore : 0;
+ MaximumAccuracy.Value = maximumBaseScore > 0 ? (currentBaseScore + (maximumBaseScore - currentMaximumBaseScore)) / maximumBaseScore : 1;
+
+ double comboProgress = maximumComboPortion > 0 ? currentComboPortion / maximumComboPortion : 1;
+ double accuracyProcess = maximumAccuracyJudgementCount > 0 ? (double)currentAccuracyJudgementCount / maximumAccuracyJudgementCount : 1;
+
+ TotalScore.Value = (long)Math.Round(ComputeTotalScore(comboProgress, accuracyProcess, currentBonusPortion) * scoreMultiplier);
}
- ///
- /// Computes the accuracy of a given .
- ///
- /// The to compute the total score of.
- /// The score's accuracy.
- [Pure]
- public double ComputeAccuracy(ScoreInfo scoreInfo)
+ protected virtual double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{
- if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
- throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
-
- // We only extract scoring values from the score's statistics. This is because accuracy is always relative to the point of pass or fail rather than relative to the whole beatmap.
- extractScoringValues(scoreInfo.Statistics, out var current, out var maximum);
-
- return maximum.BaseScore > 0 ? (double)current.BaseScore / maximum.BaseScore : 1;
- }
-
- ///
- /// Computes the total score of a given .
- ///
- ///
- /// Does not require to have been called before use.
- ///
- /// The to represent the score as.
- /// The to compute the total score of.
- /// The total score in the given .
- [Pure]
- public long ComputeScore(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}\".");
-
- extractScoringValues(scoreInfo, out var current, out var maximum);
-
- return computeScore(mode, current, maximum);
- }
-
- ///
- /// Computes the total score from scoring values.
- ///
- /// The to represent the score as.
- /// The current scoring values.
- /// The maximum scoring values.
- /// The total score computed from the given scoring values.
- [Pure]
- private long computeScore(ScoringMode mode, ScoringValues current, ScoringValues maximum)
- {
- double accuracyRatio = maximum.BaseScore > 0 ? (double)current.BaseScore / maximum.BaseScore : 1;
- double comboRatio = maximum.MaxCombo > 0 ? (double)current.MaxCombo / maximum.MaxCombo : 1;
- return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.CountBasicHitObjects);
- }
-
- ///
- /// Computes the total score from individual scoring components.
- ///
- /// The to represent the score as.
- /// The accuracy percentage achieved by the player.
- /// The portion of the max combo achieved by the player.
- /// The total bonus score.
- /// The total number of basic (non-tick and non-bonus) hitobjects in the beatmap.
- /// The total score computed from the given scoring component ratios.
- [Pure]
- public long ComputeScore(ScoringMode mode, double accuracyRatio, double comboRatio, long bonusScore, int totalBasicHitObjects)
- {
- double accuracyScore = accuracyPortion * accuracyRatio;
- double comboScore = comboPortion * comboRatio;
- double rawScore = (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier;
-
- switch (mode)
- {
- default:
- case ScoringMode.Standardised:
- return (long)Math.Round(rawScore);
-
- 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 scaledRawScore = rawScore / max_score;
- return (long)Math.Round(Math.Pow(scaledRawScore * Math.Max(1, totalBasicHitObjects), 2) * ClassicScoreMultiplier);
- }
+ return 700000 * comboProgress +
+ 300000 * Math.Pow(Accuracy.Value, 10) * accuracyProgress +
+ bonusPortion;
}
///
@@ -389,16 +318,24 @@ namespace osu.Game.Rulesets.Scoring
if (storeResults)
{
- maximumScoringValues = currentScoringValues;
+ maximumBaseScore = currentBaseScore;
+
+ maximumComboPortion = currentComboPortion;
+ maximumAccuracyJudgementCount = currentAccuracyJudgementCount;
maximumResultCounts.Clear();
maximumResultCounts.AddRange(scoreResultCounts);
+
+ MaximumTotalScore = TotalScore.Value;
}
scoreResultCounts.Clear();
- currentScoringValues = default;
- currentMaximumScoringValues = default;
+ currentBaseScore = 0;
+ currentMaximumBaseScore = 0;
+ currentAccuracyJudgementCount = 0;
+ currentComboPortion = 0;
+ currentBonusPortion = 0;
TotalScore.Value = 0;
Accuracy.Value = 1;
@@ -428,7 +365,7 @@ namespace osu.Game.Rulesets.Scoring
score.MaximumStatistics[result] = maximumResultCounts.GetValueOrDefault(result);
// Populate total score after everything else.
- score.TotalScore = ComputeScore(ScoringMode.Standardised, score);
+ score.TotalScore = TotalScore.Value;
}
///
@@ -452,126 +389,36 @@ namespace osu.Game.Rulesets.Scoring
if (frame.Header == null)
return;
- extractScoringValues(frame.Header.Statistics, out var current, out var maximum);
- currentScoringValues.BaseScore = current.BaseScore;
- currentScoringValues.MaxCombo = frame.Header.MaxCombo;
- currentMaximumScoringValues.BaseScore = maximum.BaseScore;
- currentMaximumScoringValues.MaxCombo = maximum.MaxCombo;
-
Combo.Value = frame.Header.Combo;
HighestCombo.Value = frame.Header.MaxCombo;
+ TotalScore.Value = frame.Header.TotalScore;
scoreResultCounts.Clear();
scoreResultCounts.AddRange(frame.Header.Statistics);
+ SetScoreProcessorStatistics(frame.Header.ScoreProcessorStatistics);
+
updateScore();
OnResetFromReplayFrame?.Invoke();
}
- #region ScoringValue extraction
-
- ///
- /// Applies a best-effort extraction of hit statistics into .
- ///
- ///
- /// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
- ///
- /// - The maximum will always be 0.
- /// - The current and maximum will always be the same value.
- ///
- /// Consumers are expected to more accurately fill in the above values through external means.
- ///
- /// Ensure to fill in the maximum for use in
- /// .
- ///
- ///
- /// The score to extract scoring values from.
- /// The "current" scoring values, representing the hit statistics as they appear.
- /// The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.
- [Pure]
- private void extractScoringValues(ScoreInfo scoreInfo, out ScoringValues current, out ScoringValues maximum)
+ public ScoreProcessorStatistics GetScoreProcessorStatistics() => new ScoreProcessorStatistics
{
- extractScoringValues(scoreInfo.Statistics, out current, out maximum);
- current.MaxCombo = scoreInfo.MaxCombo;
+ MaximumBaseScore = currentMaximumBaseScore,
+ BaseScore = currentBaseScore,
+ AccuracyJudgementCount = currentAccuracyJudgementCount,
+ ComboPortion = currentComboPortion,
+ BonusPortion = currentBonusPortion
+ };
- if (scoreInfo.MaximumStatistics.Count > 0)
- extractScoringValues(scoreInfo.MaximumStatistics, out _, out maximum);
- }
-
- ///
- /// Applies a best-effort extraction of hit statistics into .
- ///
- ///
- /// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
- ///
- /// - The current will always be 0.
- /// - The maximum will always be 0.
- /// - The current and maximum will always be the same value.
- ///
- /// Consumers are expected to more accurately fill in the above values (especially the current ) via external means (e.g. ).
- ///
- /// The hit statistics to extract scoring values from.
- /// The "current" scoring values, representing the hit statistics as they appear.
- /// The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.
- [Pure]
- private void extractScoringValues(IReadOnlyDictionary statistics, out ScoringValues current, out ScoringValues maximum)
+ public void SetScoreProcessorStatistics(ScoreProcessorStatistics statistics)
{
- current = default;
- maximum = default;
-
- foreach ((HitResult result, int count) in statistics)
- {
- if (!result.IsScorable())
- continue;
-
- if (result.IsBonus())
- current.BonusScore += count * Judgement.ToNumericResult(result);
-
- if (result.AffectsAccuracy())
- {
- // The maximum result of this judgement if it wasn't a miss.
- // E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT).
- HitResult maxResult;
-
- switch (result)
- {
- case HitResult.LargeTickHit:
- case HitResult.LargeTickMiss:
- maxResult = HitResult.LargeTickHit;
- break;
-
- case HitResult.SmallTickHit:
- case HitResult.SmallTickMiss:
- maxResult = HitResult.SmallTickHit;
- break;
-
- default:
- maxResult = maxBasicResult ??= Ruleset.GetHitResults().MaxBy(kvp => Judgement.ToNumericResult(kvp.result)).result;
- break;
- }
-
- current.BaseScore += count * Judgement.ToNumericResult(result);
- maximum.BaseScore += count * Judgement.ToNumericResult(maxResult);
- }
-
- if (result.AffectsCombo())
- maximum.MaxCombo += count;
-
- if (result.IsBasic())
- {
- current.CountBasicHitObjects += count;
- maximum.CountBasicHitObjects += count;
- }
- }
- }
-
- #endregion
-
- protected override void Dispose(bool isDisposing)
- {
- base.Dispose(isDisposing);
- hitEvents.Clear();
+ currentMaximumBaseScore = statistics.MaximumBaseScore;
+ currentBaseScore = statistics.BaseScore;
+ currentAccuracyJudgementCount = statistics.AccuracyJudgementCount;
+ currentComboPortion = statistics.ComboPortion;
+ currentBonusPortion = statistics.BonusPortion;
}
#region Static helper methods
@@ -630,30 +477,10 @@ namespace osu.Game.Rulesets.Scoring
#endregion
- ///
- /// Stores the required scoring data that fulfils the minimum requirements for a to calculate score.
- ///
- private struct ScoringValues
+ protected override void Dispose(bool isDisposing)
{
- ///
- /// The sum of all "basic" scoring values. See: and .
- ///
- public long BaseScore;
-
- ///
- /// The sum of all "bonus" scoring values. See: and .
- ///
- public long BonusScore;
-
- ///
- /// The highest achieved combo.
- ///
- public int MaxCombo;
-
- ///
- /// The count of "basic" s. See: .
- ///
- public int CountBasicHitObjects;
+ base.Dispose(isDisposing);
+ hitEvents.Clear();
}
}
@@ -665,4 +492,46 @@ namespace osu.Game.Rulesets.Scoring
[LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.ClassicScoreDisplay))]
Classic
}
+
+ [Serializable]
+ [MessagePackObject]
+ public class ScoreProcessorStatistics
+ {
+ ///
+ /// The sum of all accuracy-affecting judgements at the current point in time.
+ ///
+ ///
+ /// Used to compute accuracy.
+ /// See: and .
+ ///
+ [Key(0)]
+ public double BaseScore { get; set; }
+
+ ///
+ /// The maximum sum of accuracy-affecting judgements at the current point in time.
+ ///
+ ///
+ /// Used to compute accuracy.
+ ///
+ [Key(1)]
+ public double MaximumBaseScore { get; set; }
+
+ ///
+ /// The count of accuracy-affecting judgements at the current point in time.
+ ///
+ [Key(2)]
+ public int AccuracyJudgementCount { get; set; }
+
+ ///
+ /// The combo score at the current point in time.
+ ///
+ [Key(3)]
+ public double ComboPortion { get; set; }
+
+ ///
+ /// The bonus score at the current point in time.
+ ///
+ [Key(4)]
+ public double BonusPortion { get; set; }
+ }
}
diff --git a/osu.Game/Scoring/IScoreInfo.cs b/osu.Game/Scoring/IScoreInfo.cs
index 289679a724..ffc30384d2 100644
--- a/osu.Game/Scoring/IScoreInfo.cs
+++ b/osu.Game/Scoring/IScoreInfo.cs
@@ -15,6 +15,9 @@ namespace osu.Game.Scoring
{
IUser User { get; }
+ ///
+ /// The standardised total score.
+ ///
long TotalScore { get; }
int MaxCombo { get; }
diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs
index e42f6caf26..84bf6d15f6 100644
--- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs
+++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs
@@ -3,13 +3,72 @@
#nullable disable
+using System;
using System.Collections.Generic;
+using System.Linq;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Scoring.Legacy
{
public static class ScoreInfoExtensions
{
+ public static long GetDisplayScore(this ScoreProcessor scoreProcessor, ScoringMode mode)
+ => getDisplayScore(scoreProcessor.Ruleset.RulesetInfo.OnlineID, scoreProcessor.TotalScore.Value, mode, scoreProcessor.MaximumStatistics);
+
+ public static long GetDisplayScore(this ScoreInfo scoreInfo, ScoringMode mode)
+ => getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics);
+
+ private static long getDisplayScore(int rulesetId, long score, ScoringMode mode, IReadOnlyDictionary maximumStatistics)
+ {
+ if (mode == ScoringMode.Standardised)
+ return score;
+
+ int maxBasicJudgements = maximumStatistics
+ .Where(k => k.Key.IsBasic())
+ .Select(k => k.Value)
+ .DefaultIfEmpty(0)
+ .Sum();
+
+ // 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 scaledRawScore = score / ScoreProcessor.MAX_SCORE;
+
+ return (long)Math.Round(Math.Pow(scaledRawScore * Math.Max(1, maxBasicJudgements), 2) * getStandardisedToClassicMultiplier(rulesetId));
+ }
+
+ ///
+ /// Returns a ballpark multiplier which gives a similar "feel" for how large scores should get when displayed in "classic" mode.
+ /// This is different per ruleset to match the different algorithms used in the scoring implementation.
+ ///
+ private static double getStandardisedToClassicMultiplier(int rulesetId)
+ {
+ double multiplier;
+
+ switch (rulesetId)
+ {
+ // For non-legacy rulesets, just go with the same as the osu! ruleset.
+ // This is arbitrary, but at least allows the setting to do something to the score.
+ default:
+ case 0:
+ multiplier = 36;
+ break;
+
+ case 1:
+ multiplier = 22;
+ break;
+
+ case 2:
+ multiplier = 28;
+ break;
+
+ case 3:
+ multiplier = 16;
+ break;
+ }
+
+ return multiplier;
+ }
+
public static int? GetCountGeki(this ScoreInfo scoreInfo)
{
switch (scoreInfo.Ruleset.OnlineID)
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index 3e6d09b74a..d5509538fd 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -20,6 +20,7 @@ using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring;
using osu.Game.Online.API;
+using osu.Game.Scoring.Legacy;
namespace osu.Game.Scoring
{
@@ -74,7 +75,7 @@ namespace osu.Game.Scoring
/// The array of s to reorder.
/// The given ordered by decreasing total score.
public IEnumerable OrderByTotalScore(IEnumerable scores)
- => scores.OrderByDescending(s => GetTotalScore(s))
+ => scores.OrderByDescending(s => s.TotalScore)
.ThenBy(s => s.OnlineID)
// Local scores may not have an online ID. Fall back to date in these cases.
.ThenBy(s => s.Date);
@@ -87,7 +88,7 @@ namespace osu.Game.Scoring
///
/// The to retrieve the bindable for.
/// The bindable containing the total score.
- public Bindable GetBindableTotalScore([NotNull] ScoreInfo score) => new TotalScoreBindable(score, this, configManager);
+ public Bindable GetBindableTotalScore([NotNull] ScoreInfo score) => new TotalScoreBindable(score, configManager);
///
/// Retrieves a bindable that represents the formatted total score string of a .
@@ -99,25 +100,6 @@ namespace osu.Game.Scoring
/// The bindable containing the formatted total score string.
public Bindable GetBindableTotalScoreString([NotNull] ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score));
- ///
- /// Retrieves the total score of a in the given .
- ///
- /// The to calculate the total score of.
- /// The to return the total score as.
- /// The total score.
- public long GetTotalScore([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised)
- {
- // TODO: This is required for playlist aggregate scores. They should likely not be getting here in the first place.
- if (string.IsNullOrEmpty(score.BeatmapInfo.MD5Hash))
- return score.TotalScore;
-
- var ruleset = score.Ruleset.CreateInstance();
- var scoreProcessor = ruleset.CreateScoreProcessor();
- scoreProcessor.Mods.Value = score.Mods;
-
- return scoreProcessor.ComputeScore(mode, score);
- }
-
///
/// Retrieves the maximum achievable combo for the provided score.
///
@@ -136,12 +118,11 @@ namespace osu.Game.Scoring
/// Creates a new .
///
/// The to provide the total score of.
- /// The .
/// The config.
- public TotalScoreBindable(ScoreInfo score, ScoreManager scoreManager, OsuConfigManager configManager)
+ public TotalScoreBindable(ScoreInfo score, OsuConfigManager configManager)
{
configManager?.BindWith(OsuSetting.ScoreDisplayMode, scoringMode);
- scoringMode.BindValueChanged(mode => Value = scoreManager.GetTotalScore(score, mode.NewValue), true);
+ scoringMode.BindValueChanged(mode => Value = score.GetDisplayScore(mode.NewValue), true);
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
index c642b9f29f..00bd1a7019 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs
@@ -52,6 +52,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private readonly Container colouredComponents;
private readonly Container sampleComponents;
private readonly OsuSpriteText comboIndexText;
+ private readonly SamplePointPiece samplePointPiece;
+ private readonly DifficultyPointPiece? difficultyPointPiece;
[Resolved]
private ISkinSource skin { get; set; } = null!;
@@ -126,7 +128,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (item is IHasSliderVelocity)
{
- AddInternal(new DifficultyPointPiece(Item)
+ AddInternal(difficultyPointPiece = new DifficultyPointPiece(Item)
{
Anchor = Anchor.TopLeft,
Origin = Anchor.BottomCentre
@@ -271,6 +273,25 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public override Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.TopLeft;
+ protected override bool ComputeIsMaskedAway(RectangleF maskingBounds)
+ {
+ // Since children are exceeding the component size, we need to use a custom quad to compute whether it should be masked away.
+
+ // If the component isn't considered masked away by itself, there's no need to apply custom logic.
+ if (!base.ComputeIsMaskedAway(maskingBounds))
+ return false;
+
+ // If the component is considered masked away, we'll use children to create an extended quad that encapsulates all parts of this blueprint
+ // to ensure it doesn't pop in and out of existence abruptly when scrolling the timeline.
+ var rect = RectangleF.Union(ScreenSpaceDrawQuad.AABBFloat, circle.ScreenSpaceDrawQuad.AABBFloat);
+ rect = RectangleF.Union(rect, samplePointPiece.ScreenSpaceDrawQuad.AABBFloat);
+
+ if (difficultyPointPiece != null)
+ rect = RectangleF.Union(rect, difficultyPointPiece.ScreenSpaceDrawQuad.AABBFloat);
+
+ return !Precision.AlmostIntersects(maskingBounds, rect);
+ }
+
private partial class Tick : Circle
{
public Tick()
diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
index 3d80248306..37b50b4863 100644
--- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
@@ -132,7 +132,12 @@ namespace osu.Game.Screens.OnlinePlay
this.ScaleTo(1, 250, Easing.OutSine);
Debug.Assert(screenStack.CurrentScreen != null);
- screenStack.CurrentScreen.OnResuming(e);
+
+ // if a subscreen was pushed to the nested stack while the stack was not present, this path will proxy `OnResuming()`
+ // to the subscreen before `OnEntering()` can even be called for the subscreen, breaking ordering expectations.
+ // to work around this, do not proxy resume to screens that haven't loaded yet.
+ if ((screenStack.CurrentScreen as Drawable)?.IsLoaded == true)
+ screenStack.CurrentScreen.OnResuming(e);
base.OnResuming(e);
}
@@ -143,7 +148,12 @@ namespace osu.Game.Screens.OnlinePlay
this.FadeOut(250);
Debug.Assert(screenStack.CurrentScreen != null);
- screenStack.CurrentScreen.OnSuspending(e);
+
+ // if a subscreen was pushed to the nested stack while the stack was not present, this path will proxy `OnSuspending()`
+ // to the subscreen before `OnEntering()` can even be called for the subscreen, breaking ordering expectations.
+ // to work around this, do not proxy suspend to screens that haven't loaded yet.
+ if ((screenStack.CurrentScreen as Drawable)?.IsLoaded == true)
+ screenStack.CurrentScreen.OnSuspending(e);
}
public override bool OnExiting(ScreenExitEvent e)
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
index 0c25a32259..b0e4585986 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
@@ -6,14 +6,12 @@
using System;
using System.Diagnostics;
using System.Linq;
-using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Screens;
using osu.Game.Extensions;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
@@ -63,13 +61,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
return new PlaylistsResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem, true);
}
- protected override async Task PrepareScoreForResultsAsync(Score score)
- {
- await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
-
- Score.ScoreInfo.TotalScore = ScoreProcessor.ComputeScore(ScoringMode.Standardised, Score.ScoreInfo);
- }
-
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
index 07b80feb3e..4ac2f1afda 100644
--- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
+++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
@@ -1,18 +1,17 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osu.Game.Utils;
@@ -48,7 +47,7 @@ namespace osu.Game.Screens.Play.HUD
public Bindable Expanded = new Bindable();
- private OsuSpriteText positionText, scoreText, accuracyText, comboText, usernameText;
+ private OsuSpriteText positionText = null!, scoreText = null!, accuracyText = null!, comboText = null!, usernameText = null!;
public BindableLong TotalScore { get; } = new BindableLong();
public BindableDouble Accuracy { get; } = new BindableDouble(1);
@@ -56,6 +55,13 @@ namespace osu.Game.Screens.Play.HUD
public BindableBool HasQuit { get; } = new BindableBool();
public Bindable DisplayOrder { get; } = new Bindable();
+ private Func? getDisplayScoreFunction;
+
+ public Func GetDisplayScore
+ {
+ set => getDisplayScoreFunction = value;
+ }
+
public Color4? BackgroundColour { get; set; }
public Color4? TextColour { get; set; }
@@ -82,40 +88,43 @@ namespace osu.Game.Screens.Play.HUD
}
}
- [CanBeNull]
- public IUser User { get; }
+ public IUser? User { get; }
///
/// Whether this score is the local user or a replay player (and should be focused / always visible).
///
public readonly bool Tracked;
- private Container mainFillContainer;
+ private Container mainFillContainer = null!;
- private Box centralFill;
+ private Box centralFill = null!;
- private Container backgroundPaddingAdjustContainer;
+ private Container backgroundPaddingAdjustContainer = null!;
- private GridContainer gridContainer;
+ private GridContainer gridContainer = null!;
- private Container scoreComponents;
+ private Container scoreComponents = null!;
+
+ private IBindable scoreDisplayMode = null!;
///
/// Creates a new .
///
/// The score's player.
/// Whether the player is the local user or a replay player.
- public GameplayLeaderboardScore([CanBeNull] IUser user, bool tracked)
+ public GameplayLeaderboardScore(IUser? user, bool tracked)
{
User = user;
Tracked = tracked;
AutoSizeAxes = Axes.X;
Height = PANEL_HEIGHT;
+
+ GetDisplayScore = _ => TotalScore.Value;
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OsuColour colours, OsuConfigManager osuConfigManager)
{
Container avatarContainer;
@@ -234,7 +243,7 @@ namespace osu.Game.Screens.Play.HUD
Origin = Anchor.CentreLeft,
Colour = Color4.White,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
- Text = User?.Username,
+ Text = User?.Username ?? string.Empty,
Truncate = true,
Shadow = false,
}
@@ -286,7 +295,9 @@ namespace osu.Game.Screens.Play.HUD
LoadComponentAsync(new DrawableAvatar(User), avatarContainer.Add);
- TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true);
+ scoreDisplayMode = osuConfigManager.GetBindable(OsuSetting.ScoreDisplayMode);
+ scoreDisplayMode.BindValueChanged(_ => updateScore());
+ TotalScore.BindValueChanged(_ => updateScore(), true);
Accuracy.BindValueChanged(v =>
{
@@ -313,6 +324,8 @@ namespace osu.Game.Screens.Play.HUD
FinishTransforms(true);
}
+ private void updateScore() => scoreText.Text = (getDisplayScoreFunction?.Invoke(scoreDisplayMode.Value) ?? TotalScore.Value).ToString("N0");
+
private void changeExpandedState(ValueChangedEvent expanded)
{
if (expanded.NewValue)
diff --git a/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs b/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs
index a11cccd97c..a086aa6d72 100644
--- a/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs
+++ b/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs
@@ -1,20 +1,21 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring.Legacy;
namespace osu.Game.Screens.Play.HUD
{
public abstract partial class GameplayScoreCounter : ScoreCounter
{
- private Bindable scoreDisplayMode;
+ private Bindable scoreDisplayMode = null!;
+
+ private Bindable totalScoreBindable = null!;
protected GameplayScoreCounter()
: base(6)
@@ -24,6 +25,9 @@ namespace osu.Game.Screens.Play.HUD
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, ScoreProcessor scoreProcessor)
{
+ totalScoreBindable = scoreProcessor.TotalScore.GetBoundCopy();
+ totalScoreBindable.BindValueChanged(_ => updateDisplayScore());
+
scoreDisplayMode = config.GetBindable(OsuSetting.ScoreDisplayMode);
scoreDisplayMode.BindValueChanged(scoreMode =>
{
@@ -40,9 +44,11 @@ namespace osu.Game.Screens.Play.HUD
default:
throw new ArgumentOutOfRangeException(nameof(scoreMode));
}
+
+ updateDisplayScore();
}, true);
- Current.BindTo(scoreProcessor.TotalScore);
+ void updateDisplayScore() => Current.Value = scoreProcessor.GetDisplayScore(scoreDisplayMode.Value);
}
}
}
diff --git a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs
index 428390f90c..1a5d7fd9a8 100644
--- a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs
+++ b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs
@@ -1,9 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System;
using osu.Framework.Bindables;
+using osu.Game.Rulesets.Scoring;
namespace osu.Game.Screens.Play.HUD
{
@@ -20,5 +20,12 @@ namespace osu.Game.Screens.Play.HUD
/// Lower numbers will appear higher in cases of ties.
///
Bindable DisplayOrder { get; }
+
+ ///
+ /// A custom function which handles converting a score to a display score using a provide .
+ ///
+ ///
+ /// If no function is provided, will be used verbatim.
+ Func GetDisplayScore { set; }
}
}
diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
index 620f3718c2..922def6174 100644
--- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
+++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
@@ -98,6 +98,7 @@ namespace osu.Game.Screens.Play.HUD
var trackedUser = UserScores[user.Id];
var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id);
+ leaderboardScore.GetDisplayScore = trackedUser.ScoreProcessor.GetDisplayScore;
leaderboardScore.Accuracy.BindTo(trackedUser.ScoreProcessor.Accuracy);
leaderboardScore.TotalScore.BindTo(trackedUser.ScoreProcessor.TotalScore);
leaderboardScore.Combo.BindTo(trackedUser.ScoreProcessor.Combo);
diff --git a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs
index 9f92880919..e9bb1d2101 100644
--- a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs
+++ b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -10,6 +9,7 @@ using osu.Game.Configuration;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
+using osu.Game.Scoring.Legacy;
using osu.Game.Screens.Select;
using osu.Game.Users;
@@ -27,15 +27,9 @@ namespace osu.Game.Screens.Play.HUD
public readonly IBindableList Scores = new BindableList();
- // hold references to ensure bindables are updated.
- private readonly List> scoreBindables = new List>();
-
[Resolved]
private ScoreProcessor scoreProcessor { get; set; } = null!;
- [Resolved]
- private ScoreManager scoreManager { get; set; } = null!;
-
///
/// Whether the leaderboard should be visible regardless of the configuration value.
/// This is true by default, but can be changed.
@@ -70,7 +64,6 @@ namespace osu.Game.Screens.Play.HUD
private void showScores()
{
Clear();
- scoreBindables.Clear();
if (!Scores.Any())
return;
@@ -79,12 +72,8 @@ namespace osu.Game.Screens.Play.HUD
{
var score = Add(s.User, false);
- var bindableTotal = scoreManager.GetBindableTotalScore(s);
-
- // Direct binding not possible due to differing types (see https://github.com/ppy/osu/issues/20298).
- bindableTotal.BindValueChanged(total => score.TotalScore.Value = total.NewValue, true);
- scoreBindables.Add(bindableTotal);
-
+ score.GetDisplayScore = s.GetDisplayScore;
+ score.TotalScore.Value = s.TotalScore;
score.Accuracy.Value = s.Accuracy;
score.Combo.Value = s.MaxCombo;
score.DisplayOrder.Value = s.OnlineID > 0 ? s.OnlineID : s.Date.ToUnixTimeSeconds();
@@ -92,6 +81,7 @@ namespace osu.Game.Screens.Play.HUD
ILeaderboardScore local = Add(trackingUser, true);
+ local.GetDisplayScore = scoreProcessor.GetDisplayScore;
local.TotalScore.BindTarget = scoreProcessor.TotalScore;
local.Accuracy.BindTarget = scoreProcessor.Accuracy;
local.Combo.BindTarget = scoreProcessor.HighestCombo;
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 5174adfc06..18ea9d0acb 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -237,9 +237,6 @@ namespace osu.Game.Screens.Play
dependencies.CacheAs(HealthProcessor);
- if (!ScoreProcessor.Mode.Disabled)
- config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode);
-
InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime);
AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer));
diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs
index 29dec42083..b75f3d86ff 100644
--- a/osu.Game/Screens/Ranking/ScorePanelList.cs
+++ b/osu.Game/Screens/Ranking/ScorePanelList.cs
@@ -9,7 +9,6 @@ using System.Diagnostics;
using System.Linq;
using System.Threading;
using JetBrains.Annotations;
-using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -67,9 +66,6 @@ namespace osu.Game.Screens.Ranking
public readonly Bindable SelectedScore = new Bindable();
- [Resolved]
- private ScoreManager scoreManager { get; set; }
-
private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource();
private readonly Flow flow;
private readonly Scroll scroll;
@@ -149,7 +145,7 @@ namespace osu.Game.Screens.Ranking
var score = trackingContainer.Panel.Score;
- flow.SetLayoutPosition(trackingContainer, scoreManager.GetTotalScore(score));
+ flow.SetLayoutPosition(trackingContainer, score.TotalScore);
trackingContainer.Show();
diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs
index 1db35b3aaa..305a615102 100644
--- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs
+++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs
@@ -12,7 +12,9 @@ using osu.Framework.Utils;
using osu.Game.Online.API;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
+using osu.Game.Rulesets;
using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Tests.Visual.Spectator
@@ -44,6 +46,9 @@ namespace osu.Game.Tests.Visual.Spectator
[Resolved]
private IAPIProvider api { get; set; } = null!;
+ [Resolved]
+ private RulesetStore rulesetStore { get; set; } = null!;
+
public TestSpectatorClient()
{
OnNewFrames += (i, bundle) => lastReceivedUserFrames[i] = bundle.Frames[^1];
@@ -119,7 +124,7 @@ namespace osu.Game.Tests.Visual.Spectator
if (frames.Count == 0)
return;
- var bundle = new FrameDataBundle(new ScoreInfo { Combo = currentFrameIndex }, frames.ToArray());
+ var bundle = new FrameDataBundle(new ScoreInfo { Combo = currentFrameIndex }, new ScoreProcessor(rulesetStore.GetRuleset(0)!.CreateInstance()), frames.ToArray());
((ISpectatorClient)this).UserSentFrames(userId, bundle);
frames.Clear();
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 0fd2b0c2c5..8a941ca6c1 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,7 +36,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index e4a169f8e5..1dcece7741 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -16,6 +16,6 @@
iossimulator-x64
-
+