mirror of
https://github.com/ppy/osu.git
synced 2025-01-28 02:43:19 +08:00
Merge branch 'master' into update-multiplayer-room-diff-range
This commit is contained in:
commit
a9d4556647
38
README.md
38
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
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.513.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.531.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Logging;
|
||||
using osu.Game;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Screens.Play;
|
||||
using Squirrel;
|
||||
using Squirrel.SimpleSplat;
|
||||
using LogLevel = Squirrel.SimpleSplat.LogLevel;
|
||||
@ -36,6 +37,9 @@ namespace osu.Desktop.Updater
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ILocalUserPlayInfo? localUserInfo { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(INotificationOverlay notifications)
|
||||
{
|
||||
@ -55,6 +59,10 @@ namespace osu.Desktop.Updater
|
||||
|
||||
try
|
||||
{
|
||||
// Avoid any kind of update checking while gameplay is running.
|
||||
if (localUserInfo?.IsPlaying.Value == true)
|
||||
return false;
|
||||
|
||||
updateManager ??= new GithubUpdateManager(@"https://github.com/ppy/osu", false, github_token, @"osulazer");
|
||||
|
||||
var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false);
|
||||
|
@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
result.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||
|
||||
if (snapType.HasFlagFast(SnapType.Grids))
|
||||
if (snapType.HasFlagFast(SnapType.RelativeGrids))
|
||||
{
|
||||
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
|
||||
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
|
||||
|
@ -1,17 +1,30 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using 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));
|
||||
}
|
||||
}
|
||||
|
@ -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<double?>(ManiaRulesetSetting.ScrollTime, null);
|
||||
|
||||
if (Get<double?>(ManiaRulesetSetting.ScrollTime) is double scrollTime)
|
||||
{
|
||||
SetValue(ManiaRulesetSetting.ScrollSpeed, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime));
|
||||
SetValue<double?>(ManiaRulesetSetting.ScrollTime, null);
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
|
||||
{
|
||||
new TrackedSetting<double>(ManiaRulesetSetting.ScrollTime,
|
||||
scrollTime => new SettingDescription(
|
||||
rawValue: scrollTime,
|
||||
new TrackedSetting<int>(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
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using 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<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection)
|
||||
},
|
||||
new SettingsSlider<double, ManiaScrollSlider>
|
||||
new SettingsSlider<int, ManiaScrollSlider>
|
||||
{
|
||||
LabelText = RulesetSettingsStrings.ScrollSpeed,
|
||||
Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime),
|
||||
Current = config.GetBindable<int>(ManiaRulesetSetting.ScrollSpeed),
|
||||
KeyboardStep = 5
|
||||
},
|
||||
new SettingsCheckbox
|
||||
@ -48,9 +47,9 @@ namespace osu.Game.Rulesets.Mania
|
||||
};
|
||||
}
|
||||
|
||||
private partial class ManiaScrollSlider : RoundedSliderBar<double>
|
||||
private partial class ManiaScrollSlider : RoundedSliderBar<int>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -1,23 +1,29 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#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));
|
||||
}
|
||||
}
|
||||
|
@ -33,12 +33,12 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject>
|
||||
{
|
||||
/// <summary>
|
||||
/// The minimum time range. This occurs at a <see cref="relativeTimeRange"/> of 40.
|
||||
/// The minimum time range. This occurs at a <see cref="ManiaRulesetSetting.ScrollSpeed"/> of 40.
|
||||
/// </summary>
|
||||
public const double MIN_TIME_RANGE = 290;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum time range. This occurs at a <see cref="relativeTimeRange"/> of 1.
|
||||
/// The maximum time range. This occurs with a <see cref="ManiaRulesetSetting.ScrollSpeed"/> of 1.
|
||||
/// </summary>
|
||||
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<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
|
||||
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<BarLine>(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;
|
||||
|
||||
/// <summary>
|
||||
/// Computes a scroll time (in milliseconds) from a scroll speed in the range of 1-40.
|
||||
/// </summary>
|
||||
/// <param name="scrollSpeed">The scroll speed.</param>
|
||||
/// <returns>The scroll time.</returns>
|
||||
public static double ComputeScrollTime(int scrollSpeed) => MAX_TIME_RANGE / scrollSpeed;
|
||||
|
||||
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer();
|
||||
|
||||
|
@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
if (slider is null) return;
|
||||
|
||||
sample = new HitSampleInfo("hitwhistle", "soft", volume: 70);
|
||||
sample = new HitSampleInfo("hitwhistle", HitSampleInfo.BANK_SOFT, volume: 70);
|
||||
slider.Samples.Add(sample.With());
|
||||
});
|
||||
|
||||
|
@ -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
|
||||
|
@ -309,7 +309,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = snapProvider?.FindSnappedPositionAndTime(Parent.ToScreenSpace(e.MousePosition));
|
||||
var result = snapProvider?.FindSnappedPositionAndTime(Parent.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids);
|
||||
|
||||
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? Parent.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
|
||||
|
||||
|
@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
}
|
||||
|
||||
// Update the cursor position.
|
||||
var result = snapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position);
|
||||
var result = snapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.Body ? SnapType.GlobalGrids : SnapType.All);
|
||||
cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
|
||||
}
|
||||
else if (cursor != null)
|
||||
|
@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
// We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
|
||||
// The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
|
||||
// the time value if the proposed positions are roughly the same.
|
||||
if (snapType.HasFlagFast(SnapType.Grids) && DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
||||
if (snapType.HasFlagFast(SnapType.RelativeGrids) && DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
||||
{
|
||||
(Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
|
||||
if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1))
|
||||
@ -155,7 +155,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||
|
||||
if (snapType.HasFlagFast(SnapType.Grids))
|
||||
if (snapType.HasFlagFast(SnapType.RelativeGrids))
|
||||
{
|
||||
if (DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
||||
{
|
||||
@ -164,7 +164,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
result.ScreenSpacePosition = distanceSnapGrid.ToScreenSpace(pos);
|
||||
result.Time = time;
|
||||
}
|
||||
}
|
||||
|
||||
if (snapType.HasFlagFast(SnapType.GlobalGrids))
|
||||
{
|
||||
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
||||
{
|
||||
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));
|
||||
|
@ -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<OsuReplayFrame> 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);
|
||||
|
@ -42,14 +42,14 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
private PlayfieldAdjustmentContainer bubbleContainer = null!;
|
||||
|
||||
private DrawablePool<BubbleDrawable> bubblePool = null!;
|
||||
|
||||
private readonly Bindable<int> currentCombo = new BindableInt();
|
||||
|
||||
private float maxSize;
|
||||
private float bubbleSize;
|
||||
private double bubbleFade;
|
||||
|
||||
private readonly DrawablePool<BubbleDrawable> bubblePool = new DrawablePool<BubbleDrawable>(100);
|
||||
|
||||
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
|
||||
|
||||
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
|
||||
@ -72,6 +72,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
bubbleContainer = drawableRuleset.CreatePlayfieldAdjustmentContainer();
|
||||
|
||||
drawableRuleset.Overlays.Add(bubbleContainer);
|
||||
drawableRuleset.Overlays.Add(bubblePool = new DrawablePool<BubbleDrawable>(100));
|
||||
}
|
||||
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject drawableObject)
|
||||
|
@ -1,12 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
@ -13,6 +11,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Scoring;
|
||||
using osuTK;
|
||||
@ -36,8 +35,8 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
||||
|
||||
private const float rotation = 45;
|
||||
|
||||
private BufferedContainer bufferedGrid;
|
||||
private GridContainer pointGrid;
|
||||
private BufferedContainer bufferedGrid = null!;
|
||||
private GridContainer pointGrid = null!;
|
||||
|
||||
private readonly ScoreInfo score;
|
||||
private readonly IBeatmap playableBeatmap;
|
||||
@ -58,6 +57,8 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
const float line_extension = 0.2f;
|
||||
|
||||
InternalChild = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
@ -66,76 +67,99 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
||||
FillMode = FillMode.Fit,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(inner_portion),
|
||||
Masking = true,
|
||||
BorderThickness = line_thickness,
|
||||
BorderColour = Color4.White,
|
||||
Child = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4Extensions.FromHex("#202624")
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(inner_portion),
|
||||
Masking = true,
|
||||
BorderThickness = line_thickness,
|
||||
BorderColour = Color4.White,
|
||||
Child = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4Extensions.FromHex("#202624")
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(1),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Rotation = rotation,
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
new Circle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
EdgeSmoothness = new Vector2(1),
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Height = 2, // We're rotating along a diagonal - we don't really care how big this is.
|
||||
Width = line_thickness / 2,
|
||||
Rotation = -rotation,
|
||||
Alpha = 0.3f,
|
||||
Width = line_thickness,
|
||||
Height = inner_portion + line_extension,
|
||||
Rotation = -rotation * 2,
|
||||
Alpha = 0.6f,
|
||||
},
|
||||
new Box
|
||||
new Circle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
EdgeSmoothness = new Vector2(1),
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Height = 2, // We're rotating along a diagonal - we don't really care how big this is.
|
||||
Width = line_thickness / 2, // adjust for edgesmoothness
|
||||
Rotation = rotation
|
||||
Width = line_thickness,
|
||||
Height = inner_portion + line_extension,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "Overshoot",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Padding = new MarginPadding(3),
|
||||
RelativePositionAxes = Axes.Both,
|
||||
Y = -(inner_portion + line_extension) / 2,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "Undershoot",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Padding = new MarginPadding(3),
|
||||
RelativePositionAxes = Axes.Both,
|
||||
Y = (inner_portion + line_extension) / 2,
|
||||
},
|
||||
new Circle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativePositionAxes = Axes.Both,
|
||||
Y = -(inner_portion + line_extension) / 2,
|
||||
Margin = new MarginPadding(-line_thickness / 2),
|
||||
Width = line_thickness,
|
||||
Height = 10,
|
||||
Rotation = 45,
|
||||
},
|
||||
new Circle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativePositionAxes = Axes.Both,
|
||||
Y = -(inner_portion + line_extension) / 2,
|
||||
Margin = new MarginPadding(-line_thickness / 2),
|
||||
Width = line_thickness,
|
||||
Height = 10,
|
||||
Rotation = -45,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
new Box
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Width = 10,
|
||||
EdgeSmoothness = new Vector2(1),
|
||||
Height = line_thickness / 2, // adjust for edgesmoothness
|
||||
},
|
||||
new Box
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
EdgeSmoothness = new Vector2(1),
|
||||
Width = line_thickness / 2, // adjust for edgesmoothness
|
||||
Height = 10,
|
||||
}
|
||||
}
|
||||
},
|
||||
bufferedGrid = new BufferedContainer(cachedFrameBuffer: true)
|
||||
|
@ -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]
|
||||
|
@ -0,0 +1,337 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
public partial class TestSceneDrumSampleTriggerSource : OsuTestScene
|
||||
{
|
||||
private readonly ManualClock manualClock = new ManualClock();
|
||||
|
||||
[Cached(typeof(IScrollingInfo))]
|
||||
private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo
|
||||
{
|
||||
Direction = { Value = ScrollingDirection.Left },
|
||||
TimeRange = { Value = 200 },
|
||||
};
|
||||
|
||||
private ScrollingHitObjectContainer hitObjectContainer = null!;
|
||||
private TestDrumSampleTriggerSource triggerSource = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
hitObjectContainer = new ScrollingHitObjectContainer();
|
||||
manualClock.CurrentTime = 0;
|
||||
|
||||
Child = new Container
|
||||
{
|
||||
Clock = new FramedClock(manualClock),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
hitObjectContainer,
|
||||
triggerSource = new TestDrumSampleTriggerSource(hitObjectContainer)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestNormalHit()
|
||||
{
|
||||
AddStep("add hit with normal samples", () =>
|
||||
{
|
||||
var hit = new Hit
|
||||
{
|
||||
StartTime = 100,
|
||||
Samples = new List<HitSampleInfo>
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
|
||||
}
|
||||
};
|
||||
hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
var drawableHit = new DrawableHit(hit);
|
||||
hitObjectContainer.Add(drawableHit);
|
||||
});
|
||||
|
||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
|
||||
AddStep("seek past hit", () => manualClock.CurrentTime = 200);
|
||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSoftHit()
|
||||
{
|
||||
AddStep("add hit with soft samples", () =>
|
||||
{
|
||||
var hit = new Hit
|
||||
{
|
||||
StartTime = 100,
|
||||
Samples = new List<HitSampleInfo>
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT)
|
||||
}
|
||||
};
|
||||
hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
var drawableHit = new DrawableHit(hit);
|
||||
hitObjectContainer.Add(drawableHit);
|
||||
});
|
||||
|
||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||
|
||||
AddStep("seek past hit", () => manualClock.CurrentTime = 200);
|
||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDrumStrongHit()
|
||||
{
|
||||
AddStep("add strong hit with drum samples", () =>
|
||||
{
|
||||
var hit = new Hit
|
||||
{
|
||||
StartTime = 100,
|
||||
Samples = new List<HitSampleInfo>
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"),
|
||||
new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong
|
||||
}
|
||||
};
|
||||
hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
var drawableHit = new DrawableHit(hit);
|
||||
hitObjectContainer.Add(drawableHit);
|
||||
});
|
||||
|
||||
AddAssert("most valid object is strong nested hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit.StrongNestedHit>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||
|
||||
AddStep("seek past hit", () => manualClock.CurrentTime = 200);
|
||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNormalDrumRoll()
|
||||
{
|
||||
AddStep("add drum roll with normal samples", () =>
|
||||
{
|
||||
var drumRoll = new DrumRoll
|
||||
{
|
||||
StartTime = 100,
|
||||
EndTime = 1100,
|
||||
Samples = new List<HitSampleInfo>
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
|
||||
}
|
||||
};
|
||||
drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
var drawableDrumRoll = new DrawableDrumRoll(drumRoll);
|
||||
hitObjectContainer.Add(drawableDrumRoll);
|
||||
});
|
||||
|
||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
|
||||
AddStep("seek to middle of drum roll", () => manualClock.CurrentTime = 600);
|
||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
|
||||
AddStep("seek past drum roll", () => manualClock.CurrentTime = 1200);
|
||||
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSoftDrumRoll()
|
||||
{
|
||||
AddStep("add drum roll with soft samples", () =>
|
||||
{
|
||||
var drumRoll = new DrumRoll
|
||||
{
|
||||
StartTime = 100,
|
||||
EndTime = 1100,
|
||||
Samples = new List<HitSampleInfo>
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT)
|
||||
}
|
||||
};
|
||||
drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
var drawableDrumRoll = new DrawableDrumRoll(drumRoll);
|
||||
hitObjectContainer.Add(drawableDrumRoll);
|
||||
});
|
||||
|
||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||
|
||||
AddStep("seek to middle of drum roll", () => manualClock.CurrentTime = 600);
|
||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||
|
||||
AddStep("seek past drum roll", () => manualClock.CurrentTime = 1200);
|
||||
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDrumStrongDrumRoll()
|
||||
{
|
||||
AddStep("add strong drum roll with drum samples", () =>
|
||||
{
|
||||
var drumRoll = new DrumRoll
|
||||
{
|
||||
StartTime = 100,
|
||||
EndTime = 1100,
|
||||
Samples = new List<HitSampleInfo>
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"),
|
||||
new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong
|
||||
}
|
||||
};
|
||||
drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
var drawableDrumRoll = new DrawableDrumRoll(drumRoll);
|
||||
hitObjectContainer.Add(drawableDrumRoll);
|
||||
});
|
||||
|
||||
AddAssert("most valid object is drum roll tick's nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick.StrongNestedHit>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||
|
||||
AddStep("seek to middle of drum roll", () => manualClock.CurrentTime = 600);
|
||||
AddAssert("most valid object is drum roll tick's nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick.StrongNestedHit>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||
|
||||
AddStep("seek past drum roll", () => manualClock.CurrentTime = 1200);
|
||||
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNormalSwell()
|
||||
{
|
||||
AddStep("add swell with normal samples", () =>
|
||||
{
|
||||
var swell = new Swell
|
||||
{
|
||||
StartTime = 100,
|
||||
EndTime = 1100,
|
||||
Samples = new List<HitSampleInfo>
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
|
||||
}
|
||||
};
|
||||
swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
var drawableSwell = new DrawableSwell(swell);
|
||||
hitObjectContainer.Add(drawableSwell);
|
||||
});
|
||||
|
||||
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
|
||||
AddStep("seek to middle of swell", () => manualClock.CurrentTime = 600);
|
||||
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
|
||||
AddStep("seek past swell", () => manualClock.CurrentTime = 1200);
|
||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDrumSwell()
|
||||
{
|
||||
AddStep("add swell with drum samples", () =>
|
||||
{
|
||||
var swell = new Swell
|
||||
{
|
||||
StartTime = 100,
|
||||
EndTime = 1100,
|
||||
Samples = new List<HitSampleInfo>
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum")
|
||||
}
|
||||
};
|
||||
swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
var drawableSwell = new DrawableSwell(swell);
|
||||
hitObjectContainer.Add(drawableSwell);
|
||||
});
|
||||
|
||||
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||
|
||||
AddStep("seek to middle of swell", () => manualClock.CurrentTime = 600);
|
||||
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||
|
||||
AddStep("seek past swell", () => manualClock.CurrentTime = 1200);
|
||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
||||
}
|
||||
|
||||
private void checkSound(HitType hitType, string expectedName, string expectedBank)
|
||||
{
|
||||
AddStep($"hit {hitType}", () => triggerSource.Play(hitType));
|
||||
AddAssert($"last played sample is {expectedName}", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Single().Name, () => Is.EqualTo(expectedName));
|
||||
AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Single().Bank, () => Is.EqualTo(expectedBank));
|
||||
}
|
||||
|
||||
private partial class TestDrumSampleTriggerSource : DrumSampleTriggerSource
|
||||
{
|
||||
public ISampleInfo[]? LastPlayedSamples { get; private set; }
|
||||
|
||||
public TestDrumSampleTriggerSource(HitObjectContainer hitObjectContainer)
|
||||
: base(hitObjectContainer)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void PlaySamples(ISampleInfo[] samples)
|
||||
{
|
||||
base.PlaySamples(samples);
|
||||
LastPlayedSamples = samples;
|
||||
}
|
||||
|
||||
public new HitObject GetMostValidObject() => base.GetMostValidObject();
|
||||
}
|
||||
}
|
||||
}
|
@ -118,6 +118,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
public override bool RemoveWhenNotAlive => false;
|
||||
}
|
||||
|
||||
// Most osu!taiko hitsounds are managed by the drum (see DrumSampleTriggerSource).
|
||||
public override IEnumerable<HitSampleInfo> GetSamples() => Enumerable.Empty<HitSampleInfo>();
|
||||
}
|
||||
|
||||
public abstract partial class DrawableTaikoHitObject<TObject> : DrawableTaikoHitObject
|
||||
@ -157,9 +160,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
Content.Add(MainPiece = CreateMainPiece());
|
||||
}
|
||||
|
||||
// Most osu!taiko hitsounds are managed by the drum (see DrumSampleMapping).
|
||||
public override IEnumerable<HitSampleInfo> GetSamples() => Enumerable.Empty<HitSampleInfo>();
|
||||
|
||||
protected abstract SkinnableDrawable CreateMainPiece();
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,9 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using System.Threading;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
@ -98,7 +96,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
TickSpacing = tickSpacing,
|
||||
StartTime = t,
|
||||
IsStrong = IsStrong,
|
||||
Samples = Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToList()
|
||||
Samples = Samples
|
||||
});
|
||||
|
||||
first = false;
|
||||
@ -109,12 +107,21 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
|
||||
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
|
||||
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit(this)
|
||||
{
|
||||
StartTime = startTime,
|
||||
Samples = Samples
|
||||
};
|
||||
|
||||
public class StrongNestedHit : StrongNestedHitObject
|
||||
{
|
||||
// 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
|
||||
|
@ -33,10 +33,18 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
|
||||
public override double MaximumJudgementOffset => HitWindow;
|
||||
|
||||
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
|
||||
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit(this)
|
||||
{
|
||||
StartTime = startTime,
|
||||
Samples = Samples
|
||||
};
|
||||
|
||||
public class StrongNestedHit : StrongNestedHitObject
|
||||
{
|
||||
public StrongNestedHit(TaikoHitObject parent)
|
||||
: base(parent)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,10 +72,18 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
}
|
||||
}
|
||||
|
||||
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
|
||||
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit(this)
|
||||
{
|
||||
StartTime = startTime,
|
||||
Samples = Samples
|
||||
};
|
||||
|
||||
public class StrongNestedHit : StrongNestedHitObject
|
||||
{
|
||||
public StrongNestedHit(TaikoHitObject parent)
|
||||
: base(parent)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,13 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
/// </summary>
|
||||
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;
|
||||
|
@ -33,7 +33,10 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
for (int i = 0; i < RequiredHits; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
AddNested(new SwellTick());
|
||||
AddNested(new SwellTick
|
||||
{
|
||||
Samples = Samples
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,23 +1,44 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -253,17 +253,17 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
|
||||
var soundPoint = controlPoints.SamplePointAt(0);
|
||||
Assert.AreEqual(956, soundPoint.Time);
|
||||
Assert.AreEqual("soft", soundPoint.SampleBank);
|
||||
Assert.AreEqual(HitSampleInfo.BANK_SOFT, soundPoint.SampleBank);
|
||||
Assert.AreEqual(60, soundPoint.SampleVolume);
|
||||
|
||||
soundPoint = controlPoints.SamplePointAt(53373);
|
||||
Assert.AreEqual(53373, soundPoint.Time);
|
||||
Assert.AreEqual("soft", soundPoint.SampleBank);
|
||||
Assert.AreEqual(HitSampleInfo.BANK_SOFT, soundPoint.SampleBank);
|
||||
Assert.AreEqual(60, soundPoint.SampleVolume);
|
||||
|
||||
soundPoint = controlPoints.SamplePointAt(119637);
|
||||
Assert.AreEqual(119637, soundPoint.Time);
|
||||
Assert.AreEqual("soft", soundPoint.SampleBank);
|
||||
Assert.AreEqual(HitSampleInfo.BANK_SOFT, soundPoint.SampleBank);
|
||||
Assert.AreEqual(80, soundPoint.SampleVolume);
|
||||
|
||||
var effectPoint = controlPoints.EffectPointAt(0);
|
||||
@ -305,10 +305,10 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
Assert.That(controlPoints.EffectPointAt(2500).KiaiMode, Is.False);
|
||||
Assert.That(controlPoints.EffectPointAt(3500).KiaiMode, Is.True);
|
||||
|
||||
Assert.That(controlPoints.SamplePointAt(500).SampleBank, Is.EqualTo("drum"));
|
||||
Assert.That(controlPoints.SamplePointAt(1500).SampleBank, Is.EqualTo("drum"));
|
||||
Assert.That(controlPoints.SamplePointAt(2500).SampleBank, Is.EqualTo("normal"));
|
||||
Assert.That(controlPoints.SamplePointAt(3500).SampleBank, Is.EqualTo("drum"));
|
||||
Assert.That(controlPoints.SamplePointAt(500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_DRUM));
|
||||
Assert.That(controlPoints.SamplePointAt(1500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_DRUM));
|
||||
Assert.That(controlPoints.SamplePointAt(2500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_NORMAL));
|
||||
Assert.That(controlPoints.SamplePointAt(3500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_DRUM));
|
||||
|
||||
Assert.That(controlPoints.TimingPointAt(500).BeatLength, Is.EqualTo(500).Within(0.1));
|
||||
Assert.That(controlPoints.TimingPointAt(1500).BeatLength, Is.EqualTo(500).Within(0.1));
|
||||
|
@ -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, int> { { HitResult.Miss, 1 } }, DateTimeOffset.Now)
|
||||
Header = new FrameHeader(0, 0, 0, 0, new Dictionary<HitResult, int> { { 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<HitResult, int>(), DateTimeOffset.Now)
|
||||
Header = new FrameHeader(0, 0, 0, 0, new Dictionary<HitResult, int>(), 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]
|
||||
|
@ -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,
|
||||
|
31
osu.Game.Tests/Resources/storyboard_only_video.osu
Normal file
31
osu.Game.Tests/Resources/storyboard_only_video.osu
Normal file
@ -0,0 +1,31 @@
|
||||
osu file format v14
|
||||
|
||||
[Events]
|
||||
//Background and Video events
|
||||
0,0,"BG.jpg",0,0
|
||||
Video,0,"video.avi"
|
||||
//Break Periods
|
||||
//Storyboard Layer 0 (Background)
|
||||
//Storyboard Layer 1 (Fail)
|
||||
//Storyboard Layer 2 (Pass)
|
||||
//Storyboard Layer 3 (Foreground)
|
||||
//Storyboard Layer 4 (Overlay)
|
||||
//Storyboard Sound Samples
|
||||
|
||||
[TimingPoints]
|
||||
1674,333.333333333333,4,2,1,70,1,0
|
||||
1674,-100,4,2,1,70,0,0
|
||||
3340,-100,4,2,1,70,0,0
|
||||
3507,-100,4,2,1,70,0,0
|
||||
3673,-100,4,2,1,70,0,0
|
||||
|
||||
[Colours]
|
||||
Combo1 : 240,80,80
|
||||
Combo2 : 171,252,203
|
||||
Combo3 : 128,128,255
|
||||
Combo4 : 249,254,186
|
||||
|
||||
[HitObjects]
|
||||
148,303,1674,5,6,3:2:0:0:
|
||||
378,252,1840,1,0,0:0:0:0:
|
||||
389,270,2340,5,2,0:1:0:0:
|
@ -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<HitObject>
|
||||
@ -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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -70,39 +70,29 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
/// <param name="expectedScore">Expected score after all objects have been judged, rounded to the nearest integer.</param>
|
||||
/// <remarks>
|
||||
/// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and 50% max combo.
|
||||
/// <para>
|
||||
/// For standardised scoring, <paramref name="expectedScore"/> is calculated using the following formula:
|
||||
/// 1_000_000 * (((3 * <paramref name="hitResult"/>) / (4 * <paramref name="maxResult"/>)) * 30% + (bestCombo / maxCombo) * 70%)
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For classic scoring, <paramref name="expectedScore"/> is calculated using the following formula:
|
||||
/// <paramref name="hitResult"/> / <paramref name="maxResult"/> * 936
|
||||
/// where 936 is simplified from:
|
||||
/// 75% * 4 * 300 * (1 + 1/25)
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[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<HitObject>(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));
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// This test uses a beatmap with four small ticks and one object with the <see cref="Judgement.MaxResult"/> of <see cref="HitResult.Ok"/>.
|
||||
/// Its goal is to ensure that with the <see cref="ScoringMode"/> of <see cref="ScoringMode.Standardised"/>,
|
||||
/// small ticks contribute to the accuracy portion, but not the combo portion.
|
||||
/// In contrast, <see cref="ScoringMode.Classic"/> does not have separate combo and accuracy portion (they are multiplied by each other).
|
||||
/// </remarks>
|
||||
[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<HitObject> 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<HitResult, int> { { 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<ArgumentException>(() => scoreProcessor.ApplyBeatmap(new Beatmap
|
||||
{
|
||||
HitObjects = { new TestHitObject(HitResult.LegacyComboIncrease) }
|
||||
}));
|
||||
|
||||
ScoreInfo testScore = new ScoreInfo
|
||||
{
|
||||
MaxCombo = 1,
|
||||
Statistics = new Dictionary<HitResult, int>
|
||||
{
|
||||
{ HitResult.Great, 1 }
|
||||
},
|
||||
MaximumStatistics = new Dictionary<HitResult, int>
|
||||
{
|
||||
{ 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<HitResult, int>
|
||||
HitObjects = new List<HitObject>(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<Mod> GetModsFor(ModType type) => throw new NotImplementedException();
|
||||
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> 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
|
||||
{
|
||||
|
@ -42,7 +42,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(90, 90)
|
||||
Size = new Vector2(90, 90),
|
||||
Scale = new Vector2(3),
|
||||
}
|
||||
};
|
||||
});
|
||||
@ -64,17 +65,24 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
InputManager.MoveMouseTo(tickMarkerHead.ScreenSpaceDrawQuad.Centre);
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
});
|
||||
AddStep("move to 8 and release", () =>
|
||||
AddStep("move to 1", () => InputManager.MoveMouseTo(getPositionForDivisor(1)));
|
||||
AddStep("move to 16 and release", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(tickSliderBar.ScreenSpaceDrawQuad.Centre);
|
||||
InputManager.MoveMouseTo(getPositionForDivisor(16));
|
||||
InputManager.ReleaseButton(MouseButton.Left);
|
||||
});
|
||||
AddAssert("divisor is 8", () => bindableBeatDivisor.Value == 8);
|
||||
AddAssert("divisor is 16", () => bindableBeatDivisor.Value == 16);
|
||||
AddStep("hold marker", () => InputManager.PressButton(MouseButton.Left));
|
||||
AddStep("move to 16", () => InputManager.MoveMouseTo(getPositionForDivisor(16)));
|
||||
AddStep("move to ~10 and release", () =>
|
||||
AddStep("move to ~6 and release", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getPositionForDivisor(6));
|
||||
InputManager.ReleaseButton(MouseButton.Left);
|
||||
});
|
||||
AddAssert("divisor clamped to 8", () => bindableBeatDivisor.Value == 8);
|
||||
AddStep("move to ~10 and click", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getPositionForDivisor(10));
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
InputManager.ReleaseButton(MouseButton.Left);
|
||||
});
|
||||
AddAssert("divisor clamped to 8", () => bindableBeatDivisor.Value == 8);
|
||||
@ -82,12 +90,11 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
private Vector2 getPositionForDivisor(int divisor)
|
||||
{
|
||||
float relativePosition = (float)Math.Clamp(divisor, 0, 16) / 16;
|
||||
var sliderDrawQuad = tickSliderBar.ScreenSpaceDrawQuad;
|
||||
return new Vector2(
|
||||
sliderDrawQuad.TopLeft.X + sliderDrawQuad.Width * relativePosition,
|
||||
sliderDrawQuad.Centre.Y
|
||||
);
|
||||
float localX = (1 - 1 / (float)divisor) * tickSliderBar.UsableWidth + tickSliderBar.RangePadding;
|
||||
return tickSliderBar.ToScreenSpace(new Vector2(
|
||||
localX,
|
||||
tickSliderBar.DrawHeight / 2
|
||||
));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
private class SnapProvider : IDistanceSnapProvider
|
||||
{
|
||||
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.Grids) => new SnapResult(screenSpacePosition, 0);
|
||||
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.AllGrids) => new SnapResult(screenSpacePosition, 0);
|
||||
|
||||
public Bindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1);
|
||||
|
||||
|
@ -1,57 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.GameplayTest;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
public partial class TestSceneEditorNavigation : OsuGameTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestEditorGameplayTestAlwaysUsesOriginalRuleset()
|
||||
{
|
||||
BeatmapSetInfo beatmapSet = null!;
|
||||
|
||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
||||
|
||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||
AddUntilStep("wait for song select",
|
||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||
&& songSelect.IsLoaded);
|
||||
AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
||||
|
||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
AddStep("test gameplay", () => ((Editor)Game.ScreenStack.CurrentScreen).TestGameplay());
|
||||
|
||||
AddUntilStep("wait for player", () =>
|
||||
{
|
||||
// notifications may fire at almost any inopportune time and cause annoying test failures.
|
||||
// relentlessly attempt to dismiss any and all interfering overlays, which includes notifications.
|
||||
// this is theoretically not foolproof, but it's the best that can be done here.
|
||||
Game.CloseAllOverlays();
|
||||
return Game.ScreenStack.CurrentScreen is EditorPlayer editorPlayer && editorPlayer.IsLoaded;
|
||||
});
|
||||
|
||||
AddAssert("current ruleset is osu!", () => Game.Ruleset.Value.Equals(new OsuRuleset().RulesetInfo));
|
||||
|
||||
AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield()));
|
||||
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
|
||||
AddAssert("previous ruleset restored", () => Game.Ruleset.Value.Equals(new ManiaRuleset().RulesetInfo));
|
||||
}
|
||||
}
|
||||
}
|
@ -92,6 +92,20 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
hitObjectHasVelocity(1, 5);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUndo()
|
||||
{
|
||||
clickDifficultyPiece(1);
|
||||
velocityPopoverHasSingleValue(2);
|
||||
|
||||
setVelocityViaPopover(5);
|
||||
hitObjectHasVelocity(1, 5);
|
||||
dismissPopover();
|
||||
|
||||
AddStep("undo", () => Editor.Undo());
|
||||
hitObjectHasVelocity(1, 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleSelectionWithSameSliderVelocity()
|
||||
{
|
||||
|
@ -13,6 +13,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
@ -52,7 +53,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
Position = (OsuPlayfield.BASE_SIZE + new Vector2(100, 0)) / 2,
|
||||
Samples = new List<HitSampleInfo>
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft", volume: 60)
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT, volume: 60)
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -67,14 +68,14 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
hitObjectHasSampleBank(0, "normal");
|
||||
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
|
||||
hitObjectHasSampleBank(1, "soft");
|
||||
hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
|
||||
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
|
||||
|
||||
AddStep("remove clap addition", () => InputManager.Key(Key.R));
|
||||
|
||||
hitObjectHasSampleBank(0, "normal");
|
||||
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL);
|
||||
hitObjectHasSampleBank(1, "soft");
|
||||
hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
|
||||
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL);
|
||||
}
|
||||
|
||||
@ -89,7 +90,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
public void TestSingleSelection()
|
||||
{
|
||||
clickSamplePiece(0);
|
||||
samplePopoverHasSingleBank("normal");
|
||||
samplePopoverHasSingleBank(HitSampleInfo.BANK_NORMAL);
|
||||
samplePopoverHasSingleVolume(80);
|
||||
|
||||
dismissPopover();
|
||||
@ -99,14 +100,29 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.First()));
|
||||
|
||||
clickSamplePiece(1);
|
||||
samplePopoverHasSingleBank("soft");
|
||||
samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
|
||||
samplePopoverHasSingleVolume(60);
|
||||
|
||||
setVolumeViaPopover(90);
|
||||
hitObjectHasSampleVolume(1, 90);
|
||||
|
||||
setBankViaPopover("drum");
|
||||
hitObjectHasSampleBank(1, "drum");
|
||||
setBankViaPopover(HitSampleInfo.BANK_DRUM);
|
||||
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUndo()
|
||||
{
|
||||
clickSamplePiece(1);
|
||||
samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
|
||||
samplePopoverHasSingleVolume(60);
|
||||
|
||||
setVolumeViaPopover(90);
|
||||
hitObjectHasSampleVolume(1, 90);
|
||||
dismissPopover();
|
||||
|
||||
AddStep("undo", () => Editor.Undo());
|
||||
hitObjectHasSampleVolume(1, 60);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -155,7 +171,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleSelectionWithSameSampleBank()
|
||||
public void TestPopoverMultipleSelectionWithSameSampleBank()
|
||||
{
|
||||
AddStep("unify sample bank", () =>
|
||||
{
|
||||
@ -163,33 +179,33 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
for (int i = 0; i < h.Samples.Count; i++)
|
||||
{
|
||||
h.Samples[i] = h.Samples[i].With(newBank: "soft");
|
||||
h.Samples[i] = h.Samples[i].With(newBank: HitSampleInfo.BANK_SOFT);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
clickSamplePiece(0);
|
||||
samplePopoverHasSingleBank("soft");
|
||||
samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
|
||||
|
||||
dismissPopover();
|
||||
|
||||
clickSamplePiece(1);
|
||||
samplePopoverHasSingleBank("soft");
|
||||
samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
|
||||
|
||||
setBankViaPopover(string.Empty);
|
||||
hitObjectHasSampleBank(0, "soft");
|
||||
hitObjectHasSampleBank(1, "soft");
|
||||
samplePopoverHasSingleBank("soft");
|
||||
hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
|
||||
hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
|
||||
samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
|
||||
|
||||
setBankViaPopover("drum");
|
||||
hitObjectHasSampleBank(0, "drum");
|
||||
hitObjectHasSampleBank(1, "drum");
|
||||
samplePopoverHasSingleBank("drum");
|
||||
setBankViaPopover(HitSampleInfo.BANK_DRUM);
|
||||
hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
|
||||
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
|
||||
samplePopoverHasSingleBank(HitSampleInfo.BANK_DRUM);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleSelectionWithDifferentSampleBank()
|
||||
public void TestPopoverMultipleSelectionWithDifferentSampleBank()
|
||||
{
|
||||
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
clickSamplePiece(0);
|
||||
@ -201,14 +217,109 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
samplePopoverHasIndeterminateBank();
|
||||
|
||||
setBankViaPopover(string.Empty);
|
||||
hitObjectHasSampleBank(0, "normal");
|
||||
hitObjectHasSampleBank(1, "soft");
|
||||
hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
|
||||
hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
|
||||
samplePopoverHasIndeterminateBank();
|
||||
|
||||
setBankViaPopover("normal");
|
||||
hitObjectHasSampleBank(0, "normal");
|
||||
hitObjectHasSampleBank(1, "normal");
|
||||
samplePopoverHasSingleBank("normal");
|
||||
setBankViaPopover(HitSampleInfo.BANK_NORMAL);
|
||||
hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
|
||||
hitObjectHasSampleBank(1, HitSampleInfo.BANK_NORMAL);
|
||||
samplePopoverHasSingleBank(HitSampleInfo.BANK_NORMAL);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHotkeysMultipleSelectionWithSameSampleBank()
|
||||
{
|
||||
AddStep("unify sample bank", () =>
|
||||
{
|
||||
foreach (var h in EditorBeatmap.HitObjects)
|
||||
{
|
||||
for (int i = 0; i < h.Samples.Count; i++)
|
||||
{
|
||||
h.Samples[i] = h.Samples[i].With(newBank: HitSampleInfo.BANK_SOFT);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
|
||||
hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
|
||||
hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
|
||||
|
||||
AddStep("Press normal bank shortcut", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ShiftLeft);
|
||||
InputManager.Key(Key.W);
|
||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||
});
|
||||
|
||||
hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
|
||||
hitObjectHasSampleBank(1, HitSampleInfo.BANK_NORMAL);
|
||||
|
||||
AddStep("Press drum bank shortcut", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ShiftLeft);
|
||||
InputManager.Key(Key.R);
|
||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||
});
|
||||
|
||||
hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
|
||||
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
|
||||
|
||||
AddStep("Press auto bank shortcut", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ShiftLeft);
|
||||
InputManager.Key(Key.Q);
|
||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||
});
|
||||
|
||||
// Should be a noop.
|
||||
hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
|
||||
hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHotkeysDuringPlacement()
|
||||
{
|
||||
AddStep("Enter placement mode", () => InputManager.Key(Key.Number2));
|
||||
AddStep("Move mouse to centre", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<HitObjectComposer>().First().ScreenSpaceDrawQuad.Centre));
|
||||
|
||||
AddStep("Move between two objects", () => EditorClock.Seek(250));
|
||||
|
||||
AddStep("Press normal bank shortcut", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ShiftLeft);
|
||||
InputManager.Key(Key.W);
|
||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||
});
|
||||
|
||||
checkPlacementSample(HitSampleInfo.BANK_NORMAL);
|
||||
|
||||
AddStep("Press drum bank shortcut", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ShiftLeft);
|
||||
InputManager.Key(Key.R);
|
||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||
});
|
||||
|
||||
checkPlacementSample(HitSampleInfo.BANK_DRUM);
|
||||
|
||||
AddStep("Press auto bank shortcut", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ShiftLeft);
|
||||
InputManager.Key(Key.Q);
|
||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||
});
|
||||
|
||||
checkPlacementSample(HitSampleInfo.BANK_NORMAL);
|
||||
|
||||
AddStep("Move after second object", () => EditorClock.Seek(750));
|
||||
checkPlacementSample(HitSampleInfo.BANK_SOFT);
|
||||
|
||||
AddStep("Move to first object", () => EditorClock.Seek(0));
|
||||
checkPlacementSample(HitSampleInfo.BANK_NORMAL);
|
||||
|
||||
void checkPlacementSample(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First().Bank, () => Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () =>
|
||||
|
@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = t += spacing,
|
||||
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft") },
|
||||
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT) },
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
StartTime = t += spacing,
|
||||
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }),
|
||||
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, "soft") },
|
||||
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT) },
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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<int> getTotalScore)
|
||||
private void runForAlgorithm(string name, Color4 colour, Action applyHit, Action applyNonPerfect, Action applyMiss, Func<long> getTotalScore)
|
||||
{
|
||||
int maxCombo = sliderMaxCombo.Current.Value;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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<ScoreInfo> scores = new BindableList<ScoreInfo>();
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -8,6 +9,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.IO;
|
||||
@ -42,6 +44,18 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("Load storyboard with missing video", () => loadStoryboard("storyboard_no_video.osu"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVideoSize()
|
||||
{
|
||||
AddStep("load storyboard with only video", () =>
|
||||
{
|
||||
// LegacyStoryboardDecoder doesn't parse WidescreenStoryboard, so it is set manually
|
||||
loadStoryboard("storyboard_only_video.osu", s => s.BeatmapInfo.WidescreenStoryboard = false);
|
||||
});
|
||||
|
||||
AddAssert("storyboard is correct width", () => Precision.AlmostEquals(storyboard?.Width ?? 0f, 480 * 16 / 9f));
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
@ -102,7 +116,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
decoupledClock.ChangeSource(Beatmap.Value.Track);
|
||||
}
|
||||
|
||||
private void loadStoryboard(string filename)
|
||||
private void loadStoryboard(string filename, Action<Storyboard>? setUpStoryboard = null)
|
||||
{
|
||||
Storyboard loaded;
|
||||
|
||||
@ -113,6 +127,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
loaded = decoder.Decode(bfr);
|
||||
}
|
||||
|
||||
setUpStoryboard?.Invoke(loaded);
|
||||
|
||||
loadStoryboard(loaded);
|
||||
}
|
||||
}
|
||||
|
69
osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs
Normal file
69
osu.Game.Tests/Visual/Mods/TestSceneModAccuracyChallenge.cs
Normal file
@ -0,0 +1,69 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Mods
|
||||
{
|
||||
public partial class TestSceneModAccuracyChallenge : ModTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
|
||||
protected override TestPlayer CreateModPlayer(Ruleset ruleset)
|
||||
{
|
||||
var player = base.CreateModPlayer(ruleset);
|
||||
return player;
|
||||
}
|
||||
|
||||
protected override bool AllowFail => true;
|
||||
|
||||
[Test]
|
||||
public void TestMaximumAchievableAccuracy() =>
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ModAccuracyChallenge
|
||||
{
|
||||
MinimumAccuracy = { Value = 0.6 }
|
||||
},
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = Enumerable.Range(0, 5).Select(i => new HitCircle
|
||||
{
|
||||
StartTime = i * 250,
|
||||
Position = new Vector2(i * 50)
|
||||
}).Cast<HitObject>().ToList()
|
||||
},
|
||||
PassCondition = () => Player.GameplayState.HasFailed && Player.ScoreProcessor.JudgedHits >= 3
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestStandardAccuracy() =>
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ModAccuracyChallenge
|
||||
{
|
||||
MinimumAccuracy = { Value = 0.6 },
|
||||
AccuracyJudgeMode = { Value = ModAccuracyChallenge.AccuracyMode.Standard }
|
||||
},
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = Enumerable.Range(0, 5).Select(i => new HitCircle
|
||||
{
|
||||
StartTime = i * 250,
|
||||
Position = new Vector2(i * 50)
|
||||
}).Cast<HitObject>().ToList()
|
||||
},
|
||||
PassCondition = () => Player.GameplayState.HasFailed && Player.ScoreProcessor.JudgedHits >= 1
|
||||
});
|
||||
}
|
||||
}
|
@ -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<HitResult, int>
|
||||
{
|
||||
Statistics = new Dictionary<HitResult, int>
|
||||
{
|
||||
[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))
|
||||
|
@ -3,15 +3,61 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.GameplayTest;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Navigation
|
||||
{
|
||||
public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestEditorGameplayTestAlwaysUsesOriginalRuleset()
|
||||
{
|
||||
BeatmapSetInfo beatmapSet = null!;
|
||||
|
||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
||||
|
||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||
AddUntilStep("wait for song select",
|
||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||
&& songSelect.IsLoaded);
|
||||
AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
||||
|
||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
AddStep("test gameplay", () => ((Editor)Game.ScreenStack.CurrentScreen).TestGameplay());
|
||||
|
||||
AddUntilStep("wait for player", () =>
|
||||
{
|
||||
// notifications may fire at almost any inopportune time and cause annoying test failures.
|
||||
// relentlessly attempt to dismiss any and all interfering overlays, which includes notifications.
|
||||
// this is theoretically not foolproof, but it's the best that can be done here.
|
||||
Game.CloseAllOverlays();
|
||||
return Game.ScreenStack.CurrentScreen is EditorPlayer editorPlayer && editorPlayer.IsLoaded;
|
||||
});
|
||||
|
||||
AddAssert("current ruleset is osu!", () => Game.Ruleset.Value.Equals(new OsuRuleset().RulesetInfo));
|
||||
|
||||
AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield()));
|
||||
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
|
||||
AddAssert("previous ruleset restored", () => Game.Ruleset.Value.Equals(new ManiaRuleset().RulesetInfo));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When entering the editor, a new beatmap is created as part of the asynchronous load process.
|
||||
/// This test ensures that in the case of an early exit from the editor (ie. while it's still loading)
|
||||
@ -38,5 +84,63 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
|
||||
BeatmapSetInfo[] allBeatmapSets() => Game.Realm.Run(realm => realm.All<BeatmapSetInfo>().Where(x => !x.DeletePending).ToArray());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExitEditorWithoutSelection()
|
||||
{
|
||||
BeatmapSetInfo beatmapSet = null!;
|
||||
|
||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
||||
|
||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||
AddUntilStep("wait for song select",
|
||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||
&& songSelect.IsLoaded);
|
||||
|
||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
|
||||
AddStep("escape once", () => InputManager.Key(Key.Escape));
|
||||
|
||||
AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExitEditorWithSelection()
|
||||
{
|
||||
BeatmapSetInfo beatmapSet = null!;
|
||||
|
||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
||||
|
||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||
AddUntilStep("wait for song select",
|
||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||
&& songSelect.IsLoaded);
|
||||
|
||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
|
||||
AddStep("make selection", () =>
|
||||
{
|
||||
var beatmap = getEditorBeatmap();
|
||||
beatmap.SelectedHitObjects.AddRange(beatmap.HitObjects.Take(5));
|
||||
});
|
||||
|
||||
AddAssert("selection exists", () => getEditorBeatmap().SelectedHitObjects, () => Has.Count.GreaterThan(0));
|
||||
|
||||
AddStep("escape once", () => InputManager.Key(Key.Escape));
|
||||
|
||||
AddAssert("selection empty", () => getEditorBeatmap().SelectedHitObjects, () => Has.Count.Zero);
|
||||
|
||||
AddStep("escape again", () => InputManager.Key(Key.Escape));
|
||||
|
||||
AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor);
|
||||
}
|
||||
|
||||
private EditorBeatmap getEditorBeatmap() => ((Editor)Game.ScreenStack.CurrentScreen).ChildrenOfType<EditorBeatmap>().Single();
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.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);
|
||||
});
|
||||
|
@ -7,6 +7,7 @@ using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@ -137,8 +138,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
if (!objects.Any())
|
||||
return;
|
||||
|
||||
double firstHit = objects.First().StartTime;
|
||||
double lastHit = objects.Max(o => o.GetEndTime());
|
||||
(double firstHit, double lastHit) = BeatmapExtensions.CalculatePlayableBounds(objects);
|
||||
|
||||
if (lastHit == 0)
|
||||
lastHit = objects.Last().StartTime;
|
||||
|
@ -20,11 +20,20 @@ namespace osu.Game.Audio
|
||||
public const string HIT_FINISH = @"hitfinish";
|
||||
public const string HIT_CLAP = @"hitclap";
|
||||
|
||||
public const string BANK_NORMAL = @"normal";
|
||||
public const string BANK_SOFT = @"soft";
|
||||
public const string BANK_DRUM = @"drum";
|
||||
|
||||
/// <summary>
|
||||
/// All valid sample addition constants.
|
||||
/// </summary>
|
||||
public static IEnumerable<string> AllAdditions => new[] { HIT_WHISTLE, HIT_FINISH, HIT_CLAP };
|
||||
|
||||
/// <summary>
|
||||
/// All valid bank constants.
|
||||
/// </summary>
|
||||
public static IEnumerable<string> AllBanks => new[] { BANK_NORMAL, BANK_SOFT, BANK_DRUM };
|
||||
|
||||
/// <summary>
|
||||
/// The name of the sample to load.
|
||||
/// </summary>
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Logging;
|
||||
@ -11,7 +10,6 @@ using osu.Framework.Platform;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
@ -74,7 +72,7 @@ namespace osu.Game.Beatmaps
|
||||
var calculator = ruleset.CreateDifficultyCalculator(working);
|
||||
|
||||
beatmap.StarRating = calculator.Calculate().StarRating;
|
||||
beatmap.Length = calculateLength(working.Beatmap);
|
||||
beatmap.Length = working.Beatmap.CalculatePlayableLength();
|
||||
beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength();
|
||||
}
|
||||
|
||||
@ -82,20 +80,6 @@ namespace osu.Game.Beatmaps
|
||||
workingBeatmapCache.Invalidate(beatmapSet);
|
||||
});
|
||||
|
||||
private double calculateLength(IBeatmap b)
|
||||
{
|
||||
if (!b.HitObjects.Any())
|
||||
return 0;
|
||||
|
||||
var lastObject = b.HitObjects.Last();
|
||||
|
||||
//TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list).
|
||||
double endTime = lastObject.GetEndTime();
|
||||
double startTime = b.HitObjects.First().StartTime;
|
||||
|
||||
return endTime - startTime;
|
||||
}
|
||||
|
||||
#region Implementation of IDisposable
|
||||
|
||||
public void Dispose()
|
||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </remarks>
|
||||
public class SampleControlPoint : ControlPoint, IEquatable<SampleControlPoint>
|
||||
{
|
||||
public const string DEFAULT_BANK = "normal";
|
||||
public const string DEFAULT_BANK = HitSampleInfo.BANK_NORMAL;
|
||||
|
||||
public static readonly SampleControlPoint DEFAULT = new SampleControlPoint
|
||||
{
|
||||
|
@ -12,6 +12,7 @@ using System.Linq;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
@ -480,7 +481,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
string stringSampleSet = sampleSet.ToString().ToLowerInvariant();
|
||||
if (stringSampleSet == @"none")
|
||||
stringSampleSet = @"normal";
|
||||
stringSampleSet = HitSampleInfo.BANK_NORMAL;
|
||||
|
||||
if (timingChange)
|
||||
{
|
||||
|
@ -589,13 +589,13 @@ namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
switch (sampleBank?.ToLowerInvariant())
|
||||
{
|
||||
case "normal":
|
||||
case HitSampleInfo.BANK_NORMAL:
|
||||
return LegacySampleBank.Normal;
|
||||
|
||||
case "soft":
|
||||
case HitSampleInfo.BANK_SOFT:
|
||||
return LegacySampleBank.Soft;
|
||||
|
||||
case "drum":
|
||||
case HitSampleInfo.BANK_DRUM:
|
||||
return LegacySampleBank.Drum;
|
||||
|
||||
default:
|
||||
|
@ -104,6 +104,19 @@ namespace osu.Game.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the total milliseconds between the first and last hittable objects.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is cached to <see cref="BeatmapInfo.Length"/>, so using that is preferable when available.
|
||||
/// </remarks>
|
||||
public static double CalculatePlayableLength(this IBeatmap beatmap) => CalculatePlayableLength(beatmap.HitObjects);
|
||||
|
||||
/// <summary>
|
||||
/// Find the timestamps in milliseconds of the start and end of the playable region.
|
||||
/// </summary>
|
||||
public static (double start, double end) CalculatePlayableBounds(this IBeatmap beatmap) => CalculatePlayableBounds(beatmap.HitObjects);
|
||||
|
||||
/// <summary>
|
||||
/// Find the absolute end time of the latest <see cref="HitObject"/> in a beatmap. Will throw if beatmap contains no objects.
|
||||
/// </summary>
|
||||
@ -114,5 +127,36 @@ namespace osu.Game.Beatmaps
|
||||
/// It's not super efficient so calls should be kept to a minimum.
|
||||
/// </remarks>
|
||||
public static double GetLastObjectTime(this IBeatmap beatmap) => beatmap.HitObjects.Max(h => h.GetEndTime());
|
||||
|
||||
#region Helper methods
|
||||
|
||||
/// <summary>
|
||||
/// Find the total milliseconds between the first and last hittable objects.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is cached to <see cref="BeatmapInfo.Length"/>, so using that is preferable when available.
|
||||
/// </remarks>
|
||||
public static double CalculatePlayableLength(IEnumerable<HitObject> objects)
|
||||
{
|
||||
(double start, double end) = CalculatePlayableBounds(objects);
|
||||
|
||||
return end - start;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the timestamps in milliseconds of the start and end of the playable region.
|
||||
/// </summary>
|
||||
public static (double start, double end) CalculatePlayableBounds(IEnumerable<HitObject> objects)
|
||||
{
|
||||
if (!objects.Any())
|
||||
return (0, 0);
|
||||
|
||||
double lastObjectTime = objects.Max(o => o.GetEndTime());
|
||||
double firstObjectTime = objects.First().StartTime;
|
||||
|
||||
return (firstObjectTime, lastObjectTime);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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<ScoreInfo>().Where(s => s.DeletePending);
|
||||
|
||||
foreach (var score in pendingDeleteScores)
|
||||
realm.Remove(score);
|
||||
|
||||
var pendingDeleteSets = realm.All<BeatmapSetInfo>().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<SkinInfo>().Where(s => s.DeletePending);
|
||||
|
||||
foreach (var s in pendingDeleteSkins)
|
||||
realm.Remove(s);
|
||||
|
||||
var pendingDeletePresets = realm.All<ModPreset>().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<ScoreInfo>().Where(s => s.DeletePending);
|
||||
|
||||
foreach (var score in pendingDeleteScores)
|
||||
realm.Remove(score);
|
||||
|
||||
var pendingDeleteSets = realm.All<BeatmapSetInfo>().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<SkinInfo>().Where(s => s.DeletePending);
|
||||
|
||||
foreach (var s in pendingDeleteSkins)
|
||||
realm.Remove(s);
|
||||
|
||||
var pendingDeletePresets = realm.All<ModPreset>().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.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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);
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
public const float APPEAR_DURATION = 800;
|
||||
public const float DISAPPEAR_DURATION = 500;
|
||||
public const float SHADOW_OPACITY = 0.2f;
|
||||
|
||||
private const Easing easing_show = Easing.OutSine;
|
||||
private const Easing easing_hide = Easing.InSine;
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -89,14 +89,16 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
double floatValue = value.ToDouble(NumberFormatInfo.InvariantInfo);
|
||||
|
||||
if (DisplayAsPercentage)
|
||||
return floatValue.ToString("0%");
|
||||
|
||||
decimal decimalPrecision = normalise(CurrentNumber.Precision.ToDecimal(NumberFormatInfo.InvariantInfo), max_decimal_digits);
|
||||
|
||||
// Find the number of significant digits (we could have less than 5 after normalize())
|
||||
int significantDigits = FormatUtils.FindPrecision(decimalPrecision);
|
||||
|
||||
if (DisplayAsPercentage)
|
||||
{
|
||||
return floatValue.ToString($@"P{Math.Max(0, significantDigits - 2)}");
|
||||
}
|
||||
|
||||
string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? "-" : string.Empty;
|
||||
|
||||
return $"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}")}";
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
49
osu.Game/Localisation/GameplayMenuOverlayStrings.cs
Normal file
49
osu.Game/Localisation/GameplayMenuOverlayStrings.cs
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class GameplayMenuOverlayStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.GameplayMenuOverlay";
|
||||
|
||||
/// <summary>
|
||||
/// "Continue"
|
||||
/// </summary>
|
||||
public static LocalisableString Continue => new TranslatableString(getKey(@"continue"), @"Continue");
|
||||
|
||||
/// <summary>
|
||||
/// "Retry"
|
||||
/// </summary>
|
||||
public static LocalisableString Retry => new TranslatableString(getKey(@"retry"), @"Retry");
|
||||
|
||||
/// <summary>
|
||||
/// "Quit"
|
||||
/// </summary>
|
||||
public static LocalisableString Quit => new TranslatableString(getKey(@"quit"), @"Quit");
|
||||
|
||||
/// <summary>
|
||||
/// "failed"
|
||||
/// </summary>
|
||||
public static LocalisableString FailedHeader => new TranslatableString(getKey(@"failed_header"), @"failed");
|
||||
|
||||
/// <summary>
|
||||
/// "paused"
|
||||
/// </summary>
|
||||
public static LocalisableString PausedHeader => new TranslatableString(getKey(@"paused_header"), @"paused");
|
||||
|
||||
/// <summary>
|
||||
/// "Retry count: "
|
||||
/// </summary>
|
||||
public static LocalisableString RetryCount => new TranslatableString(getKey(@"retry_count"), @"Retry count: ");
|
||||
|
||||
/// <summary>
|
||||
/// "Song progress: "
|
||||
/// </summary>
|
||||
public static LocalisableString SongProgress => new TranslatableString(getKey(@"song_progress"), @"Song progress: ");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
@ -82,7 +82,7 @@ namespace osu.Game.Localisation
|
||||
/// <summary>
|
||||
/// "{0}ms (speed {1})"
|
||||
/// </summary>
|
||||
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}";
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Online.API;
|
||||
@ -35,6 +36,9 @@ namespace osu.Game.Online.Chat
|
||||
[Resolved]
|
||||
private ChannelManager channelManager { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
|
||||
private Bindable<bool> notifyOnUsername;
|
||||
private Bindable<bool> notifyOnPrivateMessage;
|
||||
|
||||
@ -89,8 +93,8 @@ namespace osu.Game.Online.Chat
|
||||
if (channel == null)
|
||||
return;
|
||||
|
||||
// Only send notifications, if ChatOverlay and the target channel aren't visible.
|
||||
if (chatOverlay.IsPresent && channelManager.CurrentChannel.Value == channel)
|
||||
// Only send notifications if ChatOverlay or the target channel aren't visible, or if the window is unfocused
|
||||
if (chatOverlay.IsPresent && channelManager.CurrentChannel.Value == channel && host.IsActive.Value)
|
||||
return;
|
||||
|
||||
foreach (var message in messages.OrderByDescending(m => m.Id))
|
||||
@ -99,6 +103,7 @@ namespace osu.Game.Online.Chat
|
||||
if (message.Id <= channel.LastReadId)
|
||||
return;
|
||||
|
||||
// ignore notifications triggered by local user's own chat messages
|
||||
if (message.Sender.Id == localUser.Value.Id)
|
||||
continue;
|
||||
|
||||
|
@ -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<LegacyReplayFrame> Frames { get; set; }
|
||||
|
||||
public FrameDataBundle(ScoreInfo score, IList<LegacyReplayFrame> frames)
|
||||
public FrameDataBundle(ScoreInfo score, ScoreProcessor scoreProcessor, IList<LegacyReplayFrame> frames)
|
||||
{
|
||||
Frames = frames;
|
||||
Header = new FrameHeader(score);
|
||||
Header = new FrameHeader(score, scoreProcessor.GetScoreProcessorStatistics());
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
|
@ -15,57 +15,74 @@ namespace osu.Game.Online.Spectator
|
||||
public class FrameHeader
|
||||
{
|
||||
/// <summary>
|
||||
/// The current accuracy of the score.
|
||||
/// The total score.
|
||||
/// </summary>
|
||||
[Key(0)]
|
||||
public long TotalScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current accuracy of the score.
|
||||
/// </summary>
|
||||
[Key(1)]
|
||||
public double Accuracy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current combo of the score.
|
||||
/// </summary>
|
||||
[Key(1)]
|
||||
[Key(2)]
|
||||
public int Combo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum combo achieved up to the current point in time.
|
||||
/// </summary>
|
||||
[Key(2)]
|
||||
[Key(3)]
|
||||
public int MaxCombo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cumulative hit statistics.
|
||||
/// </summary>
|
||||
[Key(3)]
|
||||
[Key(4)]
|
||||
public Dictionary<HitResult, int> Statistics { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional statistics that guides the score processor to calculate the correct score for this frame.
|
||||
/// </summary>
|
||||
[Key(5)]
|
||||
public ScoreProcessorStatistics ScoreProcessorStatistics { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time at which this frame was received by the server.
|
||||
/// </summary>
|
||||
[Key(4)]
|
||||
[Key(6)]
|
||||
public DateTimeOffset ReceivedTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Construct header summary information from a point-in-time reference to a score which is actively being played.
|
||||
/// </summary>
|
||||
/// <param name="score">The score for reference.</param>
|
||||
public FrameHeader(ScoreInfo score)
|
||||
/// <param name="statistics">The score processor statistics for the current point in time.</param>
|
||||
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<HitResult, int>(score.Statistics);
|
||||
|
||||
ScoreProcessorStatistics = statistics;
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
[SerializationConstructor]
|
||||
public FrameHeader(double accuracy, int combo, int maxCombo, Dictionary<HitResult, int> statistics, DateTimeOffset receivedTime)
|
||||
public FrameHeader(long totalScore, double accuracy, int combo, int maxCombo, Dictionary<HitResult, int> statistics, ScoreProcessorStatistics scoreProcessorStatistics, DateTimeOffset receivedTime)
|
||||
{
|
||||
TotalScore = totalScore;
|
||||
Accuracy = accuracy;
|
||||
Combo = combo;
|
||||
MaxCombo = maxCombo;
|
||||
Accuracy = accuracy;
|
||||
Statistics = statistics;
|
||||
ScoreProcessorStatistics = scoreProcessorStatistics;
|
||||
ReceivedTime = receivedTime;
|
||||
}
|
||||
}
|
||||
|
@ -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<FrameDataBundle> pendingFrameBundles = new Queue<FrameDataBundle>();
|
||||
|
||||
@ -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;
|
||||
|
@ -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
|
||||
/// <summary>
|
||||
/// The applied <see cref="Mod"/>s.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Mod> Mods => scoreProcessor?.Mods.Value ?? Array.Empty<Mod>();
|
||||
public IReadOnlyList<Mod> Mods => scoreInfo?.Mods ?? Array.Empty<Mod>();
|
||||
|
||||
public Func<ScoringMode, long> 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)
|
||||
|
@ -1136,12 +1136,22 @@ namespace osu.Game
|
||||
|
||||
if (entry.Level == LogLevel.Error)
|
||||
{
|
||||
Schedule(() => Notifications.Post(new SimpleNotification
|
||||
Schedule(() =>
|
||||
{
|
||||
Text = $"Encountered tablet error: \"{message}\"",
|
||||
Icon = FontAwesome.Solid.PenSquare,
|
||||
IconColour = Colours.RedDark,
|
||||
}));
|
||||
Notifications.Post(new SimpleNotification
|
||||
{
|
||||
Text = $"Disabling tablet support due to error: \"{message}\"",
|
||||
Icon = FontAwesome.Solid.PenSquare,
|
||||
IconColour = Colours.RedDark,
|
||||
});
|
||||
|
||||
// We only have one tablet handler currently.
|
||||
// The loop here is weakly guarding against a future where more than one is added.
|
||||
// If this is ever the case, this logic needs adjustment as it should probably only
|
||||
// disable the relevant tablet handler rather than all.
|
||||
foreach (var tabletHandler in Host.AvailableInputHandlers.OfType<ITabletHandler>())
|
||||
tabletHandler.Enabled.Value = false;
|
||||
});
|
||||
}
|
||||
else if (notifyOnWarning)
|
||||
{
|
||||
|
@ -56,6 +56,7 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
Colour = Color4.Black.Opacity(0),
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Hollow = true,
|
||||
Radius = 10
|
||||
};
|
||||
|
||||
@ -101,7 +102,7 @@ namespace osu.Game.Overlays
|
||||
protected override void PopIn()
|
||||
{
|
||||
base.PopIn();
|
||||
FadeEdgeEffectTo(0.4f, WaveContainer.APPEAR_DURATION, Easing.Out);
|
||||
FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
@ -13,7 +11,6 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@ -27,8 +24,8 @@ namespace osu.Game.Overlays.News
|
||||
|
||||
private readonly APINewsPost post;
|
||||
|
||||
private Box background;
|
||||
private TextFlowContainer main;
|
||||
private Box background = null!;
|
||||
private TextFlowContainer main = null!;
|
||||
|
||||
public NewsCard(APINewsPost post)
|
||||
{
|
||||
@ -41,12 +38,12 @@ namespace osu.Game.Overlays.News
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider, GameHost host)
|
||||
private void load(OverlayColourProvider colourProvider, OsuGame? game)
|
||||
{
|
||||
if (post.Slug != null)
|
||||
{
|
||||
TooltipText = "view in browser";
|
||||
Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug);
|
||||
Action = () => game?.OpenUrlExternally(@"/home/news/" + post.Slug);
|
||||
}
|
||||
|
||||
AddRange(new Drawable[]
|
||||
|
@ -20,7 +20,7 @@ using System.Diagnostics;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Online.Chat;
|
||||
|
||||
namespace osu.Game.Overlays.News.Sidebar
|
||||
{
|
||||
@ -59,7 +59,7 @@ namespace osu.Game.Overlays.News.Sidebar
|
||||
new PostsContainer
|
||||
{
|
||||
Expanded = { BindTarget = Expanded },
|
||||
Children = posts.Select(p => new PostButton(p)).ToArray()
|
||||
Children = posts.Select(p => new PostLink(p)).ToArray()
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -123,35 +123,14 @@ namespace osu.Game.Overlays.News.Sidebar
|
||||
}
|
||||
}
|
||||
|
||||
private partial class PostButton : OsuHoverContainer
|
||||
private partial class PostLink : LinkFlowContainer
|
||||
{
|
||||
protected override IEnumerable<Drawable> EffectTargets => new[] { text };
|
||||
|
||||
private readonly TextFlowContainer text;
|
||||
private readonly APINewsPost post;
|
||||
|
||||
public PostButton(APINewsPost post)
|
||||
public PostLink(APINewsPost post)
|
||||
: base(t => t.Font = OsuFont.GetFont(size: 12))
|
||||
{
|
||||
this.post = post;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Child = text = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 12))
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Text = post.Title
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider overlayColours, GameHost host)
|
||||
{
|
||||
IdleColour = overlayColours.Light2;
|
||||
HoverColour = overlayColours.Light1;
|
||||
|
||||
TooltipText = "view in browser";
|
||||
Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug);
|
||||
AddLink(post.Title, LinkAction.External, @"/home/news/" + post.Slug, "view in browser");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,9 +5,11 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
@ -16,6 +18,7 @@ using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using NotificationsStrings = osu.Game.Localisation.NotificationsStrings;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
@ -72,6 +75,14 @@ namespace osu.Game.Overlays
|
||||
mainContent = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Colour = Color4.Black.Opacity(0),
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 10,
|
||||
Hollow = true,
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
@ -199,6 +210,7 @@ namespace osu.Game.Overlays
|
||||
|
||||
this.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
mainContent.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
mainContent.FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out);
|
||||
|
||||
toastTray.FlushAllToasts();
|
||||
}
|
||||
@ -211,6 +223,7 @@ namespace osu.Game.Overlays
|
||||
|
||||
this.MoveToX(WIDTH, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
mainContent.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
mainContent.FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.In);
|
||||
}
|
||||
|
||||
private void notificationClosed() => Schedule(() =>
|
||||
|
@ -193,25 +193,17 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
|
||||
currentDisplay.BindValueChanged(display => Schedule(() =>
|
||||
{
|
||||
// there is no easy way to perform an atomic swap on the `resolutions` list, which is bound to the dropdown via `ItemSource`.
|
||||
// this means, if the old resolution isn't saved, the `RemoveRange()` call below will cause `resolutionDropdown.Current` to reset value,
|
||||
// therefore making it impossible to select any dropdown value other than "Default".
|
||||
// to circumvent this locally, store the old value here, so that we can attempt to restore it later.
|
||||
var oldResolution = resolutionDropdown.Current.Value;
|
||||
|
||||
resolutions.RemoveRange(1, resolutions.Count - 1);
|
||||
|
||||
if (display.NewValue != null)
|
||||
if (display.NewValue == null)
|
||||
{
|
||||
resolutions.AddRange(display.NewValue.DisplayModes
|
||||
.Where(m => m.Size.Width >= 800 && m.Size.Height >= 600)
|
||||
.OrderByDescending(m => Math.Max(m.Size.Height, m.Size.Width))
|
||||
.Select(m => m.Size)
|
||||
.Distinct());
|
||||
resolutions.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolutions.Contains(oldResolution))
|
||||
resolutionDropdown.Current.Value = oldResolution;
|
||||
resolutions.ReplaceRange(1, resolutions.Count - 1, display.NewValue.DisplayModes
|
||||
.Where(m => m.Size.Width >= 800 && m.Size.Height >= 600)
|
||||
.OrderByDescending(m => Math.Max(m.Size.Height, m.Size.Width))
|
||||
.Select(m => m.Size)
|
||||
.Distinct());
|
||||
|
||||
updateDisplaySettingsVisibility();
|
||||
}), true);
|
||||
|
@ -10,15 +10,18 @@ using System.Threading.Tasks;
|
||||
using osuTK;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
{
|
||||
@ -105,6 +108,14 @@ namespace osu.Game.Overlays
|
||||
Add(SectionsContainer = new SettingsSectionsContainer
|
||||
{
|
||||
Masking = true,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Colour = Color4.Black.Opacity(0),
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Hollow = true,
|
||||
Radius = 10
|
||||
},
|
||||
MaskingSmoothness = 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ExpandableHeader = CreateHeader(),
|
||||
SelectedSection = { BindTarget = CurrentSection },
|
||||
@ -156,6 +167,8 @@ namespace osu.Game.Overlays
|
||||
|
||||
ContentContainer.MoveToX(ExpandedPosition, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
|
||||
SectionsContainer.FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out);
|
||||
|
||||
// delay load enough to ensure it doesn't overlap with the initial animation.
|
||||
// this is done as there is still a brief stutter during load completion which is more visible if the transition is in progress.
|
||||
// the eventual goal would be to remove the need for this by splitting up load into smaller work pieces, or fixing the remaining
|
||||
@ -175,6 +188,7 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
base.PopOut();
|
||||
|
||||
SectionsContainer.FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.In);
|
||||
ContentContainer.MoveToX(-WIDTH + ExpandedPosition, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
|
||||
Sidebar?.MoveToX(-sidebar_width, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
|
@ -113,7 +113,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
if (replayGeneratingMod != null)
|
||||
screen.Push(new PlayerLoader(() => new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods))));
|
||||
}, new[] { typeof(Player), typeof(SongSelect) })
|
||||
}, new[] { typeof(Player), typeof(PlaySongSelect) })
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
|
||||
foreach (var combination in CreateDifficultyAdjustmentModCombinations())
|
||||
{
|
||||
Mod classicMod = rulesetInstance.CreateAllMods().SingleOrDefault(m => m is ModClassic);
|
||||
Mod classicMod = rulesetInstance.CreateMod<ModClassic>();
|
||||
|
||||
var finalCombination = ModUtils.FlattenMod(combination);
|
||||
if (classicMod != null)
|
||||
|
@ -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
|
||||
|
@ -77,6 +77,8 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
private FillFlowContainer togglesCollection;
|
||||
|
||||
private FillFlowContainer sampleBankTogglesCollection;
|
||||
|
||||
private IBindable<bool> hasTiming;
|
||||
private Bindable<bool> autoSeekOnPlacement;
|
||||
|
||||
@ -160,6 +162,16 @@ namespace osu.Game.Rulesets.Edit
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 5),
|
||||
},
|
||||
},
|
||||
new EditorToolboxGroup("bank (Shift-Q~R)")
|
||||
{
|
||||
Child = sampleBankTogglesCollection = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 5),
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -197,6 +209,8 @@ namespace osu.Game.Rulesets.Edit
|
||||
TernaryStates = CreateTernaryButtons().ToArray();
|
||||
togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b)));
|
||||
|
||||
sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates.Select(b => new DrawableTernaryButton(b)));
|
||||
|
||||
setSelectTool();
|
||||
|
||||
EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged;
|
||||
@ -249,7 +263,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// <summary>
|
||||
/// Create all ternary states required to be displayed to the user.
|
||||
/// </summary>
|
||||
protected virtual IEnumerable<TernaryButton> CreateTernaryButtons() => BlueprintContainer.TernaryStates;
|
||||
protected virtual IEnumerable<TernaryButton> CreateTernaryButtons() => BlueprintContainer.MainTernaryStates;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
|
||||
@ -274,7 +288,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.ShiftPressed)
|
||||
if (e.ControlPressed || e.AltPressed || e.SuperPressed)
|
||||
return false;
|
||||
|
||||
if (checkLeftToggleFromKey(e.Key, out int leftIndex))
|
||||
@ -291,7 +305,9 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
if (checkRightToggleFromKey(e.Key, out int rightIndex))
|
||||
{
|
||||
var item = togglesCollection.ElementAtOrDefault(rightIndex);
|
||||
var item = e.ShiftPressed
|
||||
? sampleBankTogglesCollection.ElementAtOrDefault(rightIndex)
|
||||
: togglesCollection.ElementAtOrDefault(rightIndex);
|
||||
|
||||
if (item is DrawableTernaryButton button)
|
||||
{
|
||||
@ -428,7 +444,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
||||
double? targetTime = null;
|
||||
|
||||
if (snapType.HasFlagFast(SnapType.Grids))
|
||||
if (snapType.HasFlagFast(SnapType.GlobalGrids))
|
||||
{
|
||||
if (playfield is ScrollingPlayfield scrollingPlayfield)
|
||||
{
|
||||
|
@ -32,6 +32,11 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// </summary>
|
||||
public PlacementState PlacementActive { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the sample bank should be taken from the previous hit object.
|
||||
/// </summary>
|
||||
public bool AutomaticBankAssignment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="HitObject"/> that is being placed.
|
||||
/// </summary>
|
||||
@ -86,11 +91,6 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// <param name="commitStart">Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments.</param>
|
||||
protected void BeginPlacement(bool commitStart = false)
|
||||
{
|
||||
// Take the hitnormal sample of the last hit object
|
||||
var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
|
||||
if (lastHitNormal != null)
|
||||
HitObject.Samples[0] = lastHitNormal;
|
||||
|
||||
placementHandler.BeginPlacement(HitObject);
|
||||
if (commitStart)
|
||||
PlacementActive = PlacementState.Active;
|
||||
@ -155,6 +155,14 @@ namespace osu.Game.Rulesets.Edit
|
||||
if (HitObject is IHasComboInformation comboInformation)
|
||||
comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation);
|
||||
}
|
||||
|
||||
if (AutomaticBankAssignment)
|
||||
{
|
||||
// Take the hitnormal sample of the last hit object
|
||||
var lastHitNormal = getPreviousHitObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
|
||||
if (lastHitNormal != null)
|
||||
HitObject.Samples[0] = lastHitNormal;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -11,8 +11,24 @@ namespace osu.Game.Rulesets.Edit
|
||||
public enum SnapType
|
||||
{
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Snapping to visible nearby objects.
|
||||
/// </summary>
|
||||
NearbyObjects = 1 << 0,
|
||||
Grids = 1 << 1,
|
||||
All = NearbyObjects | Grids,
|
||||
|
||||
/// <summary>
|
||||
/// Grids which are global to the playfield.
|
||||
/// </summary>
|
||||
GlobalGrids = 1 << 1,
|
||||
|
||||
/// <summary>
|
||||
/// Grids which are relative to other nearby hit objects.
|
||||
/// </summary>
|
||||
RelativeGrids = 1 << 2,
|
||||
|
||||
AllGrids = RelativeGrids | GlobalGrids,
|
||||
|
||||
All = NearbyObjects | GlobalGrids | RelativeGrids,
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,11 @@ namespace osu.Game.Rulesets.Judgements
|
||||
/// </summary>
|
||||
public int ComboAtJudgement { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The combo after this <see cref="JudgementResult"/> occurred.
|
||||
/// </summary>
|
||||
public int ComboAfterJudgement { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The highest combo achieved prior to this <see cref="JudgementResult"/> occurring.
|
||||
/// </summary>
|
||||
|
@ -7,6 +7,7 @@ using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Localisation.HUD;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
@ -42,30 +43,44 @@ namespace osu.Game.Rulesets.Mods
|
||||
Value = 0.9,
|
||||
};
|
||||
|
||||
private ScoreProcessor scoreProcessor = null!;
|
||||
[SettingSource("Accuracy mode", "The mode of accuracy that will trigger failure.")]
|
||||
public Bindable<AccuracyMode> AccuracyJudgeMode { get; } = new Bindable<AccuracyMode>();
|
||||
|
||||
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) => this.scoreProcessor = scoreProcessor;
|
||||
private readonly Bindable<double> currentAccuracy = new Bindable<double>();
|
||||
|
||||
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
|
||||
{
|
||||
switch (AccuracyJudgeMode.Value)
|
||||
{
|
||||
case AccuracyMode.Standard:
|
||||
currentAccuracy.BindTo(scoreProcessor.Accuracy);
|
||||
break;
|
||||
|
||||
case AccuracyMode.MaximumAchievable:
|
||||
currentAccuracy.BindTo(scoreProcessor.MaximumAccuracy);
|
||||
break;
|
||||
}
|
||||
|
||||
currentAccuracy.BindValueChanged(s =>
|
||||
{
|
||||
if (s.NewValue < MinimumAccuracy.Value)
|
||||
{
|
||||
TriggerFailure();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
|
||||
|
||||
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
|
||||
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => false;
|
||||
|
||||
public enum AccuracyMode
|
||||
{
|
||||
if (!result.Type.AffectsAccuracy())
|
||||
return false;
|
||||
[LocalisableDescription(typeof(GameplayAccuracyCounterStrings), nameof(GameplayAccuracyCounterStrings.AccuracyDisplayModeMax))]
|
||||
MaximumAchievable,
|
||||
|
||||
return getAccuracyWithImminentResultAdded(result) < MinimumAccuracy.Value;
|
||||
}
|
||||
|
||||
private double getAccuracyWithImminentResultAdded(JudgementResult result)
|
||||
{
|
||||
var score = new ScoreInfo { Ruleset = scoreProcessor.Ruleset.RulesetInfo };
|
||||
|
||||
// This is super ugly, but if we don't do it this way we will not have the most recent result added to the accuracy value.
|
||||
// Hopefully we can improve this in the future.
|
||||
scoreProcessor.PopulateScore(score);
|
||||
score.Statistics[result.Type]++;
|
||||
|
||||
return scoreProcessor.ComputeAccuracy(score);
|
||||
[LocalisableDescription(typeof(GameplayAccuracyCounterStrings), nameof(GameplayAccuracyCounterStrings.AccuracyDisplayModeStandard))]
|
||||
Standard,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
=> AffectsCombo(result) && !IsHit(result);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a <see cref="HitResult"/> increases/breaks the combo, and affects the combo portion of the score.
|
||||
/// Whether a <see cref="HitResult"/> increases or breaks the combo.
|
||||
/// </summary>
|
||||
public static bool AffectsCombo(this HitResult result)
|
||||
{
|
||||
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when this <see cref="ScoreProcessor"/> was reset from a replay frame.
|
||||
/// </summary>
|
||||
@ -78,39 +77,72 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
public readonly BindableInt HighestCombo = new BindableInt();
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ScoringMode"/> used to calculate scores.
|
||||
/// </summary>
|
||||
public readonly Bindable<ScoringMode> Mode = new Bindable<ScoringMode>();
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="HitEvent"/>s collected during gameplay thus far.
|
||||
/// Intended for use with various statistics displays.
|
||||
/// </summary>
|
||||
public IReadOnlyList<HitEvent> HitEvents => hitEvents;
|
||||
|
||||
/// <summary>
|
||||
/// The default portion of <see cref="max_score"/> awarded for hitting <see cref="HitObject"/>s accurately. Defaults to 30%.
|
||||
/// </summary>
|
||||
protected virtual double DefaultAccuracyPortion => 0.3;
|
||||
|
||||
/// <summary>
|
||||
/// The default portion of <see cref="max_score"/> awarded for achieving a high combo. Default to 70%.
|
||||
/// </summary>
|
||||
protected virtual double DefaultComboPortion => 0.7;
|
||||
|
||||
/// <summary>
|
||||
/// An arbitrary multiplier to scale scores in the <see cref="ScoringMode.Classic"/> scoring mode.
|
||||
/// </summary>
|
||||
protected virtual double ClassicScoreMultiplier => 36;
|
||||
|
||||
/// <summary>
|
||||
/// The ruleset this score processor is valid for.
|
||||
/// </summary>
|
||||
public readonly Ruleset Ruleset;
|
||||
|
||||
private readonly double accuracyPortion;
|
||||
private readonly double comboPortion;
|
||||
/// <summary>
|
||||
/// The maximum achievable total score.
|
||||
/// </summary>
|
||||
public long MaximumTotalScore { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum sum of accuracy-affecting judgements at the current point in time.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used to compute accuracy.
|
||||
/// </remarks>
|
||||
private double currentMaximumBaseScore;
|
||||
|
||||
/// <summary>
|
||||
/// The sum of all accuracy-affecting judgements at the current point in time.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used to compute accuracy.
|
||||
/// </remarks>
|
||||
private double currentBaseScore;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum sum of all accuracy-affecting judgements in the beatmap.
|
||||
/// </summary>
|
||||
private double maximumBaseScore;
|
||||
|
||||
/// <summary>
|
||||
/// The count of all accuracy-affecting judgements in the beatmap.
|
||||
/// </summary>
|
||||
private int maximumAccuracyJudgementCount;
|
||||
|
||||
/// <summary>
|
||||
/// The count of accuracy-affecting judgements at the current point in time.
|
||||
/// </summary>
|
||||
private int currentAccuracyJudgementCount;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum combo score in the beatmap.
|
||||
/// </summary>
|
||||
private double maximumComboPortion;
|
||||
|
||||
/// <summary>
|
||||
/// The combo score at the current point in time.
|
||||
/// </summary>
|
||||
private double currentComboPortion;
|
||||
|
||||
/// <summary>
|
||||
/// The bonus score at the current point in time.
|
||||
/// </summary>
|
||||
private double currentBonusPortion;
|
||||
|
||||
/// <summary>
|
||||
/// The total score multiplier.
|
||||
/// </summary>
|
||||
private double scoreMultiplier = 1;
|
||||
|
||||
public Dictionary<HitResult, int> MaximumStatistics
|
||||
{
|
||||
@ -123,27 +155,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
}
|
||||
}
|
||||
|
||||
private ScoringValues maximumScoringValues;
|
||||
|
||||
/// <summary>
|
||||
/// Scoring values for the current play assuming all perfect hits.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is only used to determine the accuracy with respect to the current point in time for an ongoing play session.
|
||||
/// </remarks>
|
||||
private ScoringValues currentMaximumScoringValues;
|
||||
|
||||
/// <summary>
|
||||
/// Scoring values for the current play.
|
||||
/// </summary>
|
||||
private ScoringValues currentScoringValues;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum <see cref="HitResult"/> of a basic (non-tick and non-bonus) hitobject.
|
||||
/// Only populated via <see cref="ComputeScore(osu.Game.Rulesets.Scoring.ScoringMode,osu.Game.Scoring.ScoreInfo)"/> or <see cref="ResetFromReplayFrame"/>.
|
||||
/// </summary>
|
||||
private HitResult? maxBasicResult;
|
||||
|
||||
private bool beatmapApplied;
|
||||
|
||||
private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>();
|
||||
@ -152,18 +163,10 @@ namespace osu.Game.Rulesets.Scoring
|
||||
private readonly List<HitEvent> hitEvents = new List<HitEvent>();
|
||||
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++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="HitEvent"/> that describes a <see cref="JudgementResult"/>.
|
||||
/// </summary>
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the accuracy of a given <see cref="ScoreInfo"/>.
|
||||
/// </summary>
|
||||
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
|
||||
/// <returns>The score's accuracy.</returns>
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the total score of a given <see cref="ScoreInfo"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Does not require <see cref="JudgementProcessor.ApplyBeatmap"/> to have been called before use.
|
||||
/// </remarks>
|
||||
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
|
||||
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
|
||||
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the total score from scoring values.
|
||||
/// </summary>
|
||||
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
|
||||
/// <param name="current">The current scoring values.</param>
|
||||
/// <param name="maximum">The maximum scoring values.</param>
|
||||
/// <returns>The total score computed from the given scoring values.</returns>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the total score from individual scoring components.
|
||||
/// </summary>
|
||||
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
|
||||
/// <param name="accuracyRatio">The accuracy percentage achieved by the player.</param>
|
||||
/// <param name="comboRatio">The portion of the max combo achieved by the player.</param>
|
||||
/// <param name="bonusScore">The total bonus score.</param>
|
||||
/// <param name="totalBasicHitObjects">The total number of basic (non-tick and non-bonus) hitobjects in the beatmap.</param>
|
||||
/// <returns>The total score computed from the given scoring component ratios.</returns>
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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
|
||||
|
||||
/// <summary>
|
||||
/// Applies a best-effort extraction of hit statistics into <see cref="ScoringValues"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
|
||||
/// <list type="bullet">
|
||||
/// <item>The maximum <see cref="ScoringValues.BonusScore"/> will always be 0.</item>
|
||||
/// <item>The current and maximum <see cref="ScoringValues.CountBasicHitObjects"/> will always be the same value.</item>
|
||||
/// </list>
|
||||
/// Consumers are expected to more accurately fill in the above values through external means.
|
||||
/// <para>
|
||||
/// <b>Ensure</b> to fill in the maximum <see cref="ScoringValues.CountBasicHitObjects"/> for use in
|
||||
/// <see cref="computeScore(osu.Game.Rulesets.Scoring.ScoringMode,ScoringValues,ScoringValues)"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="scoreInfo">The score to extract scoring values from.</param>
|
||||
/// <param name="current">The "current" scoring values, representing the hit statistics as they appear.</param>
|
||||
/// <param name="maximum">The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.</param>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a best-effort extraction of hit statistics into <see cref="ScoringValues"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
|
||||
/// <list type="bullet">
|
||||
/// <item>The current <see cref="ScoringValues.MaxCombo"/> will always be 0.</item>
|
||||
/// <item>The maximum <see cref="ScoringValues.BonusScore"/> will always be 0.</item>
|
||||
/// <item>The current and maximum <see cref="ScoringValues.CountBasicHitObjects"/> will always be the same value.</item>
|
||||
/// </list>
|
||||
/// Consumers are expected to more accurately fill in the above values (especially the current <see cref="ScoringValues.MaxCombo"/>) via external means (e.g. <see cref="ScoreInfo"/>).
|
||||
/// </remarks>
|
||||
/// <param name="statistics">The hit statistics to extract scoring values from.</param>
|
||||
/// <param name="current">The "current" scoring values, representing the hit statistics as they appear.</param>
|
||||
/// <param name="maximum">The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.</param>
|
||||
[Pure]
|
||||
private void extractScoringValues(IReadOnlyDictionary<HitResult, int> 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
|
||||
|
||||
/// <summary>
|
||||
/// Stores the required scoring data that fulfils the minimum requirements for a <see cref="ScoreProcessor"/> to calculate score.
|
||||
/// </summary>
|
||||
private struct ScoringValues
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
/// <summary>
|
||||
/// The sum of all "basic" <see cref="HitObject"/> scoring values. See: <see cref="HitResultExtensions.IsBasic"/> and <see cref="Judgement.ToNumericResult"/>.
|
||||
/// </summary>
|
||||
public long BaseScore;
|
||||
|
||||
/// <summary>
|
||||
/// The sum of all "bonus" <see cref="HitObject"/> scoring values. See: <see cref="HitResultExtensions.IsBonus"/> and <see cref="Judgement.ToNumericResult"/>.
|
||||
/// </summary>
|
||||
public long BonusScore;
|
||||
|
||||
/// <summary>
|
||||
/// The highest achieved combo.
|
||||
/// </summary>
|
||||
public int MaxCombo;
|
||||
|
||||
/// <summary>
|
||||
/// The count of "basic" <see cref="HitObject"/>s. See: <see cref="HitResultExtensions.IsBasic"/>.
|
||||
/// </summary>
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// The sum of all accuracy-affecting judgements at the current point in time.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used to compute accuracy.
|
||||
/// See: <see cref="HitResultExtensions.IsBasic"/> and <see cref="Judgement.ToNumericResult"/>.
|
||||
/// </remarks>
|
||||
[Key(0)]
|
||||
public double BaseScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum sum of accuracy-affecting judgements at the current point in time.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used to compute accuracy.
|
||||
/// </remarks>
|
||||
[Key(1)]
|
||||
public double MaximumBaseScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The count of accuracy-affecting judgements at the current point in time.
|
||||
/// </summary>
|
||||
[Key(2)]
|
||||
public int AccuracyJudgementCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The combo score at the current point in time.
|
||||
/// </summary>
|
||||
[Key(3)]
|
||||
public double ComboPortion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The bonus score at the current point in time.
|
||||
/// </summary>
|
||||
[Key(4)]
|
||||
public double BonusPortion { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.UI
|
||||
PlaySamples(samples);
|
||||
}
|
||||
|
||||
protected void PlaySamples(ISampleInfo[] samples) => Schedule(() =>
|
||||
protected virtual void PlaySamples(ISampleInfo[] samples) => Schedule(() =>
|
||||
{
|
||||
var hitSound = getNextSample();
|
||||
hitSound.Samples = samples;
|
||||
|
@ -1,9 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
@ -40,29 +38,16 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
|
||||
|
||||
public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
|
||||
{
|
||||
// Find the control point relating to the position.
|
||||
Debug.Assert(controlPoints.Count > 0);
|
||||
|
||||
// Iterate over control points and find the most relevant for the provided position.
|
||||
// Note: Due to velocity adjustments, overlapping control points will provide multiple valid time values for a single position
|
||||
// As such, this operation provides unexpected results by using the latter of the control points.
|
||||
var relevantControlPoint = controlPoints.LastOrDefault(cp => PositionAt(cp.Time, currentTime, timeRange, scrollLength) <= position) ?? controlPoints.First();
|
||||
|
||||
int i = 0;
|
||||
float pos = 0;
|
||||
float positionAtControlPoint = PositionAt(relevantControlPoint.Time, currentTime, timeRange, scrollLength);
|
||||
|
||||
for (; i < controlPoints.Count; i++)
|
||||
{
|
||||
float lastPos = pos;
|
||||
pos = PositionAt(controlPoints[i].Time, currentTime, timeRange, scrollLength);
|
||||
|
||||
if (pos > position)
|
||||
{
|
||||
i--;
|
||||
pos = lastPos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
i = Math.Clamp(i, 0, controlPoints.Count - 1);
|
||||
|
||||
return controlPoints[i].Time + (position - pos) * timeRange / controlPoints[i].Multiplier / scrollLength;
|
||||
return relevantControlPoint.Time + (position - positionAtControlPoint) * timeRange / relevantControlPoint.Multiplier / scrollLength;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
|
@ -15,6 +15,9 @@ namespace osu.Game.Scoring
|
||||
{
|
||||
IUser User { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The standardised total score.
|
||||
/// </summary>
|
||||
long TotalScore { get; }
|
||||
|
||||
int MaxCombo { get; }
|
||||
|
@ -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<HitResult, int> 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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)
|
||||
|
@ -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
|
||||
/// <param name="scores">The array of <see cref="ScoreInfo"/>s to reorder.</param>
|
||||
/// <returns>The given <paramref name="scores"/> ordered by decreasing total score.</returns>
|
||||
public IEnumerable<ScoreInfo> OrderByTotalScore(IEnumerable<ScoreInfo> 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
|
||||
/// </remarks>
|
||||
/// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param>
|
||||
/// <returns>The bindable containing the total score.</returns>
|
||||
public Bindable<long> GetBindableTotalScore([NotNull] ScoreInfo score) => new TotalScoreBindable(score, this, configManager);
|
||||
public Bindable<long> GetBindableTotalScore([NotNull] ScoreInfo score) => new TotalScoreBindable(score, configManager);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a bindable that represents the formatted total score string of a <see cref="ScoreInfo"/>.
|
||||
@ -99,25 +100,6 @@ namespace osu.Game.Scoring
|
||||
/// <returns>The bindable containing the formatted total score string.</returns>
|
||||
public Bindable<string> GetBindableTotalScoreString([NotNull] ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score));
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the total score of a <see cref="ScoreInfo"/> in the given <see cref="ScoringMode"/>.
|
||||
/// </summary>
|
||||
/// <param name="score">The <see cref="ScoreInfo"/> to calculate the total score of.</param>
|
||||
/// <param name="mode">The <see cref="ScoringMode"/> to return the total score as.</param>
|
||||
/// <returns>The total score.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the maximum achievable combo for the provided score.
|
||||
/// </summary>
|
||||
@ -136,12 +118,11 @@ namespace osu.Game.Scoring
|
||||
/// Creates a new <see cref="TotalScoreBindable"/>.
|
||||
/// </summary>
|
||||
/// <param name="score">The <see cref="ScoreInfo"/> to provide the total score of.</param>
|
||||
/// <param name="scoreManager">The <see cref="ScoreManager"/>.</param>
|
||||
/// <param name="configManager">The config.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,12 +154,15 @@ namespace osu.Game.Screens.Edit
|
||||
/// </summary>
|
||||
/// <param name="index">The 0-based beat index.</param>
|
||||
/// <param name="beatDivisor">The beat divisor.</param>
|
||||
/// <param name="validDivisors">The list of valid divisors which can be chosen from. Assumes ordered from low to high. Defaults to <see cref="PREDEFINED_DIVISORS"/> if omitted.</param>
|
||||
/// <returns>The applicable divisor.</returns>
|
||||
public static int GetDivisorForBeatIndex(int index, int beatDivisor)
|
||||
public static int GetDivisorForBeatIndex(int index, int beatDivisor, int[] validDivisors = null)
|
||||
{
|
||||
validDivisors ??= PREDEFINED_DIVISORS;
|
||||
|
||||
int beat = index % beatDivisor;
|
||||
|
||||
foreach (int divisor in PREDEFINED_DIVISORS)
|
||||
foreach (int divisor in validDivisors)
|
||||
{
|
||||
if ((beat * divisor) % beatDivisor == 0)
|
||||
return divisor;
|
||||
|
@ -383,7 +383,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
CurrentNumber.BindTo(this.beatDivisor = beatDivisor);
|
||||
|
||||
Padding = new MarginPadding { Horizontal = 5 };
|
||||
RangePadding = 5;
|
||||
Padding = new MarginPadding { Horizontal = RangePadding };
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -398,15 +399,20 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
ClearInternal();
|
||||
CurrentNumber.ValueChanged -= moveMarker;
|
||||
|
||||
foreach (int divisor in beatDivisor.ValidDivisors.Value.Presets)
|
||||
int largestDivisor = beatDivisor.ValidDivisors.Value.Presets.Last();
|
||||
|
||||
for (int tickIndex = 0; tickIndex <= largestDivisor; tickIndex++)
|
||||
{
|
||||
AddInternal(new Tick(divisor)
|
||||
int divisor = BindableBeatDivisor.GetDivisorForBeatIndex(tickIndex, largestDivisor, (int[])beatDivisor.ValidDivisors.Value.Presets);
|
||||
bool isSolidTick = divisor * (largestDivisor - tickIndex) == largestDivisor;
|
||||
|
||||
AddInternal(new Tick(divisor, isSolidTick)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.Centre,
|
||||
RelativePositionAxes = Axes.Both,
|
||||
Colour = BindableBeatDivisor.GetColourFor(divisor, colours),
|
||||
X = getMappedPosition(divisor),
|
||||
X = tickIndex / (float)largestDivisor,
|
||||
});
|
||||
}
|
||||
|
||||
@ -418,6 +424,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
private void moveMarker(ValueChangedEvent<int> divisor)
|
||||
{
|
||||
marker.MoveToX(getMappedPosition(divisor.NewValue), 100, Easing.OutQuint);
|
||||
|
||||
foreach (Tick tick in InternalChildren.OfType<Tick>().Where(t => !t.AlwaysDisplayed))
|
||||
{
|
||||
tick.FadeTo(divisor.NewValue % tick.Divisor == 0 ? 0.2f : 0f, 100, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateValue(float value)
|
||||
@ -483,13 +494,22 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
OnUserChange(Current.Value);
|
||||
}
|
||||
|
||||
private float getMappedPosition(float divisor) => MathF.Pow((divisor - 1) / (beatDivisor.ValidDivisors.Value.Presets.Last() - 1), 0.90f);
|
||||
private float getMappedPosition(float divisor) => 1 - 1 / divisor;
|
||||
|
||||
private partial class Tick : Circle
|
||||
{
|
||||
public Tick(int divisor)
|
||||
public readonly bool AlwaysDisplayed;
|
||||
|
||||
public readonly int Divisor;
|
||||
|
||||
public Tick(int divisor, bool alwaysDisplayed)
|
||||
{
|
||||
AlwaysDisplayed = alwaysDisplayed;
|
||||
Divisor = divisor;
|
||||
|
||||
Size = new Vector2(6f, 12) * BindableBeatDivisor.GetSize(divisor);
|
||||
Alpha = alwaysDisplayed ? 1 : 0;
|
||||
|
||||
InternalChild = new Box { RelativeSizeAxes = Axes.Both };
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
@ -26,7 +27,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// A container which provides a "blueprint" display of items.
|
||||
/// Includes selection and manipulation support via a <see cref="Components.SelectionHandler{T}"/>.
|
||||
/// </summary>
|
||||
public abstract partial class BlueprintContainer<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>
|
||||
public abstract partial class BlueprintContainer<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IKeyBindingHandler<GlobalAction>
|
||||
where T : class
|
||||
{
|
||||
protected DragBox DragBox { get; private set; }
|
||||
@ -91,7 +92,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
};
|
||||
|
||||
SelectionHandler = CreateSelectionHandler();
|
||||
SelectionHandler.DeselectAll = DeselectAll;
|
||||
SelectionHandler.SelectedItems.BindTo(SelectedItems);
|
||||
|
||||
AddRangeInternal(new[]
|
||||
@ -279,6 +279,30 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
return false;
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.Back:
|
||||
if (SelectedItems.Count > 0)
|
||||
{
|
||||
DeselectAll();
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||
{
|
||||
}
|
||||
|
||||
#region Blueprint Addition/Removal
|
||||
|
||||
protected virtual void AddBlueprintFor(T item)
|
||||
|
@ -14,6 +14,8 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
@ -56,7 +58,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
TernaryStates = CreateTernaryButtons().ToArray();
|
||||
MainTernaryStates = CreateTernaryButtons().ToArray();
|
||||
SampleBankTernaryStates = createSampleBankTernaryButtons().ToArray();
|
||||
|
||||
AddInternal(new DrawableRulesetDependenciesProvidingContainer(Composer.Ruleset)
|
||||
{
|
||||
@ -78,9 +81,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
// we own SelectionHandler so don't need to worry about making bindable copies (for simplicity)
|
||||
foreach (var kvp in SelectionHandler.SelectionSampleStates)
|
||||
{
|
||||
kvp.Value.BindValueChanged(_ => updatePlacementSamples());
|
||||
}
|
||||
|
||||
foreach (var kvp in SelectionHandler.SelectionBankStates)
|
||||
kvp.Value.BindValueChanged(_ => updatePlacementSamples());
|
||||
}
|
||||
|
||||
protected override void TransferBlueprintFor(HitObject hitObject, DrawableHitObject drawableObject)
|
||||
@ -166,6 +170,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
foreach (var kvp in SelectionHandler.SelectionSampleStates)
|
||||
sampleChanged(kvp.Key, kvp.Value.Value);
|
||||
|
||||
foreach (var kvp in SelectionHandler.SelectionBankStates)
|
||||
bankChanged(kvp.Key, kvp.Value.Value);
|
||||
}
|
||||
|
||||
private void sampleChanged(string sampleName, TernaryState state)
|
||||
@ -190,12 +197,24 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
}
|
||||
}
|
||||
|
||||
private void bankChanged(string bankName, TernaryState state)
|
||||
{
|
||||
if (CurrentPlacement == null) return;
|
||||
|
||||
if (bankName == EditorSelectionHandler.HIT_BANK_AUTO)
|
||||
CurrentPlacement.AutomaticBankAssignment = state == TernaryState.True;
|
||||
else if (state == TernaryState.True)
|
||||
CurrentPlacement.HitObject.Samples = CurrentPlacement.HitObject.Samples.Select(s => s.With(newBank: bankName)).ToList();
|
||||
}
|
||||
|
||||
public readonly Bindable<TernaryState> NewCombo = new Bindable<TernaryState> { Description = "New Combo" };
|
||||
|
||||
/// <summary>
|
||||
/// A collection of states which will be displayed to the user in the toolbox.
|
||||
/// </summary>
|
||||
public TernaryButton[] TernaryStates { get; private set; }
|
||||
public TernaryButton[] MainTernaryStates { get; private set; }
|
||||
|
||||
public TernaryButton[] SampleBankTernaryStates { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create all ternary states required to be displayed to the user.
|
||||
@ -209,6 +228,39 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
yield return new TernaryButton(kvp.Value, kvp.Key.Replace("hit", string.Empty).Titleize(), () => getIconForSample(kvp.Key));
|
||||
}
|
||||
|
||||
private IEnumerable<TernaryButton> createSampleBankTernaryButtons()
|
||||
{
|
||||
foreach (var kvp in SelectionHandler.SelectionBankStates)
|
||||
yield return new TernaryButton(kvp.Value, kvp.Key.Titleize(), () => getIconForBank(kvp.Key));
|
||||
}
|
||||
|
||||
private Drawable getIconForBank(string sampleName)
|
||||
{
|
||||
return new Container
|
||||
{
|
||||
Size = new Vector2(30, 20),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new SpriteIcon
|
||||
{
|
||||
Size = new Vector2(8),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Icon = FontAwesome.Solid.VolumeOff
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
X = 10,
|
||||
Y = -1,
|
||||
Font = OsuFont.Default.With(weight: FontWeight.Bold, size: 20),
|
||||
Text = $"{char.ToUpperInvariant(sampleName.First())}"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Drawable getIconForSample(string sampleName)
|
||||
{
|
||||
switch (sampleName)
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -20,6 +21,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
public partial class EditorSelectionHandler : SelectionHandler<HitObject>
|
||||
{
|
||||
/// <summary>
|
||||
/// A special bank name that is only used in the editor UI.
|
||||
/// When selected and in placement mode, the bank of the last hit object will always be used.
|
||||
/// </summary>
|
||||
public const string HIT_BANK_AUTO = "auto";
|
||||
|
||||
[Resolved]
|
||||
protected EditorBeatmap EditorBeatmap { get; private set; }
|
||||
|
||||
@ -48,11 +55,83 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// </summary>
|
||||
public readonly Dictionary<string, Bindable<TernaryState>> SelectionSampleStates = new Dictionary<string, Bindable<TernaryState>>();
|
||||
|
||||
/// <summary>
|
||||
/// The state of each sample bank type for all selected hitobjects.
|
||||
/// </summary>
|
||||
public readonly Dictionary<string, Bindable<TernaryState>> SelectionBankStates = new Dictionary<string, Bindable<TernaryState>>();
|
||||
|
||||
/// <summary>
|
||||
/// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions)
|
||||
/// </summary>
|
||||
private void createStateBindables()
|
||||
{
|
||||
foreach (string bankName in HitSampleInfo.AllBanks.Prepend(HIT_BANK_AUTO))
|
||||
{
|
||||
var bindable = new Bindable<TernaryState>
|
||||
{
|
||||
Description = bankName.Titleize()
|
||||
};
|
||||
|
||||
bindable.ValueChanged += state =>
|
||||
{
|
||||
switch (state.NewValue)
|
||||
{
|
||||
case TernaryState.False:
|
||||
if (SelectedItems.Count == 0)
|
||||
{
|
||||
// Ensure that if this is the last selected bank, it should remain selected.
|
||||
if (SelectionBankStates.Values.All(b => b.Value == TernaryState.False))
|
||||
bindable.Value = TernaryState.True;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Auto should never apply when there is a selection made.
|
||||
// This is also required to stop a bindable feedback loop when a HitObject has zero samples (and LINQ `All` below becomes true).
|
||||
if (bankName == HIT_BANK_AUTO)
|
||||
break;
|
||||
|
||||
// Never remove a sample bank.
|
||||
// These are basically radio buttons, not toggles.
|
||||
if (SelectedItems.All(h => h.Samples.All(s => s.Bank == bankName)))
|
||||
bindable.Value = TernaryState.True;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case TernaryState.True:
|
||||
if (SelectedItems.Count == 0)
|
||||
{
|
||||
// Ensure the user can't stack multiple bank selections when there's no hitobject selection.
|
||||
// Note that in normal scenarios this is sorted out by the feedback from applying the bank to the selected objects.
|
||||
foreach (var other in SelectionBankStates.Values)
|
||||
{
|
||||
if (other != bindable)
|
||||
other.Value = TernaryState.False;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Auto should just not apply if there's a selection already made.
|
||||
// Maybe we could make it a disabled button in the future, but right now the editor buttons don't support disabled state.
|
||||
if (bankName == HIT_BANK_AUTO)
|
||||
{
|
||||
bindable.Value = TernaryState.False;
|
||||
break;
|
||||
}
|
||||
|
||||
AddSampleBank(bankName);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
SelectionBankStates[bankName] = bindable;
|
||||
}
|
||||
|
||||
// start with normal selected.
|
||||
SelectionBankStates[SampleControlPoint.DEFAULT_BANK].Value = TernaryState.True;
|
||||
|
||||
foreach (string sampleName in HitSampleInfo.AllAdditions)
|
||||
{
|
||||
var bindable = new Bindable<TernaryState>
|
||||
@ -104,12 +183,33 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
bindable.Value = GetStateFromSelection(SelectedItems, h => h.Samples.Any(s => s.Name == sampleName));
|
||||
}
|
||||
|
||||
foreach ((string bankName, var bindable) in SelectionBankStates)
|
||||
{
|
||||
bindable.Value = GetStateFromSelection(SelectedItems, h => h.Samples.All(s => s.Bank == bankName));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Ternary state changes
|
||||
|
||||
/// <summary>
|
||||
/// Adds a sample bank to all selected <see cref="HitObject"/>s.
|
||||
/// </summary>
|
||||
/// <param name="bankName">The name of the sample bank.</param>
|
||||
public void AddSampleBank(string bankName)
|
||||
{
|
||||
EditorBeatmap.PerformOnSelection(h =>
|
||||
{
|
||||
if (h.Samples.All(s => s.Bank == bankName))
|
||||
return;
|
||||
|
||||
h.Samples = h.Samples.Select(s => s.With(newBank: bankName)).ToList();
|
||||
EditorBeatmap.Update(h);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a hit sample to all selected <see cref="HitObject"/>s.
|
||||
/// </summary>
|
||||
@ -174,11 +274,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
yield return new TernaryStateToggleMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } };
|
||||
}
|
||||
|
||||
yield return new OsuMenuItem("Sound")
|
||||
yield return new OsuMenuItem("Sample")
|
||||
{
|
||||
Items = SelectionSampleStates.Select(kvp =>
|
||||
new TernaryStateToggleMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
|
||||
};
|
||||
|
||||
yield return new OsuMenuItem("Bank")
|
||||
{
|
||||
Items = SelectionBankStates.Select(kvp =>
|
||||
new TernaryStateToggleMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -22,6 +22,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
public const float BORDER_RADIUS = 3;
|
||||
|
||||
private const float button_padding = 5;
|
||||
|
||||
public Func<float, bool> OnRotation;
|
||||
public Func<Vector2, Anchor, bool> OnScale;
|
||||
public Func<Direction, bool, bool> OnFlip;
|
||||
@ -182,6 +184,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
return base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
ensureButtonsOnScreen();
|
||||
}
|
||||
|
||||
private void recreate()
|
||||
{
|
||||
if (LoadState < LoadState.Loading)
|
||||
@ -234,11 +243,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
},
|
||||
buttons = new FillFlowContainer
|
||||
{
|
||||
Y = 20,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Height = 30,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.Centre
|
||||
Margin = new MarginPadding(button_padding),
|
||||
}
|
||||
};
|
||||
|
||||
@ -352,5 +360,39 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (activeOperations++ == 0)
|
||||
OperationStarted?.Invoke();
|
||||
}
|
||||
|
||||
private void ensureButtonsOnScreen()
|
||||
{
|
||||
buttons.Position = Vector2.Zero;
|
||||
|
||||
var thisQuad = ScreenSpaceDrawQuad;
|
||||
|
||||
// Shrink the parent quad to give a bit of padding so the buttons don't stick *right* on the border.
|
||||
// AABBFloat assumes no rotation. one would hope the whole editor is not being rotated.
|
||||
var parentQuad = Parent.ScreenSpaceDrawQuad.AABBFloat.Shrink(ToLocalSpace(thisQuad.TopLeft + new Vector2(button_padding * 2)));
|
||||
|
||||
float topExcess = thisQuad.TopLeft.Y - parentQuad.TopLeft.Y;
|
||||
float bottomExcess = parentQuad.BottomLeft.Y - thisQuad.BottomLeft.Y;
|
||||
float leftExcess = thisQuad.TopLeft.X - parentQuad.TopLeft.X;
|
||||
float rightExcess = parentQuad.TopRight.X - thisQuad.TopRight.X;
|
||||
|
||||
if (topExcess + bottomExcess < buttons.Height + button_padding)
|
||||
{
|
||||
buttons.Anchor = Anchor.BottomCentre;
|
||||
buttons.Origin = Anchor.BottomCentre;
|
||||
}
|
||||
else if (topExcess > bottomExcess)
|
||||
{
|
||||
buttons.Anchor = Anchor.TopCentre;
|
||||
buttons.Origin = Anchor.BottomCentre;
|
||||
}
|
||||
else
|
||||
{
|
||||
buttons.Anchor = Anchor.BottomCentre;
|
||||
buttons.Origin = Anchor.TopCentre;
|
||||
}
|
||||
|
||||
buttons.X += ToLocalSpace(thisQuad.TopLeft - new Vector2(Math.Min(0, leftExcess)) + new Vector2(Math.Min(0, rightExcess))).X;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -197,9 +197,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
#region Selection Handling
|
||||
|
||||
/// <summary>
|
||||
/// Bind an action to deselect all selected blueprints.
|
||||
/// Deselect all selected items.
|
||||
/// </summary>
|
||||
internal Action DeselectAll { private get; set; }
|
||||
protected void DeselectAll() => SelectedItems.Clear();
|
||||
|
||||
/// <summary>
|
||||
/// Handle a blueprint becoming selected.
|
||||
@ -303,7 +303,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (blueprint.IsSelected)
|
||||
return false;
|
||||
|
||||
DeselectAll?.Invoke();
|
||||
DeselectAll();
|
||||
blueprint.Select();
|
||||
return true;
|
||||
}
|
||||
@ -311,6 +311,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
protected void DeleteSelected()
|
||||
{
|
||||
DeleteItems(SelectedItems.ToArray());
|
||||
DeselectAll();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -50,6 +50,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
private readonly Container colouredComponents;
|
||||
private readonly OsuSpriteText comboIndexText;
|
||||
private readonly SamplePointPiece samplePointPiece;
|
||||
private readonly DifficultyPointPiece? difficultyPointPiece;
|
||||
|
||||
[Resolved]
|
||||
private ISkinSource skin { get; set; } = null!;
|
||||
@ -101,7 +103,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
},
|
||||
}
|
||||
},
|
||||
new SamplePointPiece(Item)
|
||||
samplePointPiece = new SamplePointPiece(Item)
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.TopCentre
|
||||
@ -118,7 +120,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
|
||||
@ -244,6 +246,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()
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@ -15,7 +16,9 @@ using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Skinning;
|
||||
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
|
||||
|
||||
@ -42,6 +45,7 @@ namespace osu.Game.Screens.Edit
|
||||
editorBeatmap.BeginChange();
|
||||
processHitObjects(result, () => newBeatmap ??= readBeatmap(newState));
|
||||
processTimingPoints(() => newBeatmap ??= readBeatmap(newState));
|
||||
processHitObjectLocalData(() => newBeatmap ??= readBeatmap(newState));
|
||||
editorBeatmap.EndChange();
|
||||
}
|
||||
|
||||
@ -87,6 +91,41 @@ namespace osu.Game.Screens.Edit
|
||||
}
|
||||
}
|
||||
|
||||
private void processHitObjectLocalData(Func<IBeatmap> getNewBeatmap)
|
||||
{
|
||||
// This method handles data that are stored in control points in the legacy format,
|
||||
// but were moved to the hitobjects themselves in lazer.
|
||||
// Specifically, the data being referred to here consists of: slider velocity and sample information.
|
||||
|
||||
// For simplicity, this implementation relies on the editor beatmap already having the same hitobjects in sequence as the new beatmap.
|
||||
// To guarantee that, `processHitObjects()` must be ran prior to this method for correct operation.
|
||||
// This is done to avoid the necessity of reimplementing/reusing parts of LegacyBeatmapDecoder that already treat this data correctly.
|
||||
|
||||
var oldObjects = editorBeatmap.HitObjects;
|
||||
var newObjects = getNewBeatmap().HitObjects;
|
||||
|
||||
Debug.Assert(oldObjects.Count == newObjects.Count);
|
||||
|
||||
foreach (var (oldObject, newObject) in oldObjects.Zip(newObjects))
|
||||
{
|
||||
// if `oldObject` and `newObject` are the same, it means that `oldObject` was inserted into `editorBeatmap` by `processHitObjects()`.
|
||||
// in that case, there is nothing to do (and some of the subsequent changes may even prove destructive).
|
||||
if (ReferenceEquals(oldObject, newObject))
|
||||
continue;
|
||||
|
||||
if (oldObject is IHasSliderVelocity oldWithVelocity && newObject is IHasSliderVelocity newWithVelocity)
|
||||
oldWithVelocity.SliderVelocity = newWithVelocity.SliderVelocity;
|
||||
|
||||
oldObject.Samples = newObject.Samples;
|
||||
|
||||
if (oldObject is IHasRepeats oldWithRepeats && newObject is IHasRepeats newWithRepeats)
|
||||
{
|
||||
oldWithRepeats.NodeSamples.Clear();
|
||||
oldWithRepeats.NodeSamples.AddRange(newWithRepeats.NodeSamples);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void findChangedIndices(DiffResult result, LegacyDecoder<Beatmap>.Section section, out List<int> removedIndices, out List<int> addedIndices)
|
||||
{
|
||||
removedIndices = new List<int>();
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user