1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 17:02:55 +08:00

Merge branch 'master' into add-mod-search-option

This commit is contained in:
Cootz 2023-06-09 16:27:18 +03:00 committed by GitHub
commit 036479dd9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
263 changed files with 3638 additions and 2076 deletions

View File

@ -2,7 +2,7 @@ blank_issues_enabled: false
contact_links: contact_links:
- name: Help - name: Help
url: https://github.com/ppy/osu/discussions/categories/q-a url: https://github.com/ppy/osu/discussions/categories/q-a
about: osu! not working as you'd expect? Not sure it's a bug? Check the Q&A section! about: osu! not working or performing as you'd expect? Not sure it's a bug? Check the Q&A section!
- name: Suggestions or feature request - name: Suggestions or feature request
url: https://github.com/ppy/osu/discussions/categories/ideas url: https://github.com/ppy/osu/discussions/categories/ideas
about: Got something you think should change or be added? Search for or start a new discussion! about: Got something you think should change or be added? Search for or start a new discussion!

View File

@ -16,21 +16,20 @@ The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Curre
## Status ## 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: 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). - 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). - 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! ## 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) | | [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: Please make sure you have the following prerequisites:
- A desktop platform with the [.NET 6.0 SDK](https://dotnet.microsoft.com/download) installed. - 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 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.
### Downloading the source code ### 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 ### 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 ### Code analysis

View File

@ -11,7 +11,7 @@
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger> <AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.521.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2023.608.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" /> <AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />

View File

@ -9,6 +9,7 @@ using osu.Framework.Logging;
using osu.Game; using osu.Game;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Screens.Play;
using Squirrel; using Squirrel;
using Squirrel.SimpleSplat; using Squirrel.SimpleSplat;
using LogLevel = Squirrel.SimpleSplat.LogLevel; using LogLevel = Squirrel.SimpleSplat.LogLevel;
@ -36,6 +37,9 @@ namespace osu.Desktop.Updater
[Resolved] [Resolved]
private OsuGameBase game { get; set; } = null!; private OsuGameBase game { get; set; } = null!;
[Resolved]
private ILocalUserPlayInfo? localUserInfo { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(INotificationOverlay notifications) private void load(INotificationOverlay notifications)
{ {
@ -55,6 +59,10 @@ namespace osu.Desktop.Updater
try 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"); updateManager ??= new GithubUpdateManager(@"https://github.com/ppy/osu", false, github_token, @"osulazer");
var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false); var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false);

View File

@ -0,0 +1,180 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Edit
{
/// <summary>
/// A grid which displays coloured beat divisor lines in proximity to the selection or placement cursor.
/// </summary>
/// <remarks>
/// This class heavily borrows from osu!mania's implementation (ManiaBeatSnapGrid).
/// If further changes are to be made, they should also be applied there.
/// If the scale of the changes are large enough, abstracting may be a good path.
/// </remarks>
public partial class CatchBeatSnapGrid : Component
{
private const double visible_range = 750;
/// <summary>
/// The range of time values of the current selection.
/// </summary>
public (double start, double end)? SelectionTimeRange
{
set
{
if (value == selectionTimeRange)
return;
selectionTimeRange = value;
lineCache.Invalidate();
}
}
[Resolved]
private EditorBeatmap beatmap { get; set; } = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private BindableBeatDivisor beatDivisor { get; set; } = null!;
private readonly Cached lineCache = new Cached();
private (double start, double end)? selectionTimeRange;
private ScrollingHitObjectContainer lineContainer = null!;
[BackgroundDependencyLoader]
private void load(HitObjectComposer composer)
{
lineContainer = new ScrollingHitObjectContainer();
((CatchPlayfield)composer.Playfield).UnderlayElements.Add(lineContainer);
beatDivisor.BindValueChanged(_ => createLines(), true);
}
protected override void Update()
{
base.Update();
if (!lineCache.IsValid)
{
lineCache.Validate();
createLines();
}
}
private readonly Stack<DrawableGridLine> availableLines = new Stack<DrawableGridLine>();
private void createLines()
{
foreach (var line in lineContainer.Objects.OfType<DrawableGridLine>())
availableLines.Push(line);
lineContainer.Clear();
if (selectionTimeRange == null)
return;
var range = selectionTimeRange.Value;
var timingPoint = beatmap.ControlPointInfo.TimingPointAt(range.start - visible_range);
double time = timingPoint.Time;
int beat = 0;
// progress time until in the visible range.
while (time < range.start - visible_range)
{
time += timingPoint.BeatLength / beatDivisor.Value;
beat++;
}
while (time < range.end + visible_range)
{
var nextTimingPoint = beatmap.ControlPointInfo.TimingPointAt(time);
// switch to the next timing point if we have reached it.
if (nextTimingPoint.Time > timingPoint.Time)
{
beat = 0;
time = nextTimingPoint.Time;
timingPoint = nextTimingPoint;
}
Color4 colour = BindableBeatDivisor.GetColourFor(
BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value), colours);
if (!availableLines.TryPop(out var line))
line = new DrawableGridLine();
line.HitObject.StartTime = time;
line.Colour = colour;
lineContainer.Add(line);
beat++;
time += timingPoint.BeatLength / beatDivisor.Value;
}
// required to update ScrollingHitObjectContainer's cache.
lineContainer.UpdateSubTree();
foreach (var line in lineContainer.Objects.OfType<DrawableGridLine>())
{
time = line.HitObject.StartTime;
if (time >= range.start && time <= range.end)
line.Alpha = 1;
else
{
double timeSeparation = time < range.start ? range.start - time : time - range.end;
line.Alpha = (float)Math.Max(0, 1 - timeSeparation / visible_range);
}
}
}
private partial class DrawableGridLine : DrawableHitObject
{
public DrawableGridLine()
: base(new HitObject())
{
RelativeSizeAxes = Axes.X;
Height = 2;
AddInternal(new Box { RelativeSizeAxes = Axes.Both });
}
[BackgroundDependencyLoader]
private void load()
{
Origin = Anchor.BottomLeft;
Anchor = Anchor.BottomLeft;
}
protected override void UpdateInitialTransforms()
{
// don't perform any fading we are handling that ourselves.
LifetimeEnd = HitObject.StartTime + visible_range;
}
}
}
}

View File

@ -33,6 +33,8 @@ namespace osu.Game.Rulesets.Catch.Edit
private InputManager inputManager = null!; private InputManager inputManager = null!;
private CatchBeatSnapGrid beatSnapGrid = null!;
private readonly BindableDouble timeRangeMultiplier = new BindableDouble(1) private readonly BindableDouble timeRangeMultiplier = new BindableDouble(1)
{ {
MinValue = 1, MinValue = 1,
@ -65,6 +67,8 @@ namespace osu.Game.Rulesets.Catch.Edit
Catcher.BASE_DASH_SPEED, -Catcher.BASE_DASH_SPEED, Catcher.BASE_DASH_SPEED, -Catcher.BASE_DASH_SPEED,
Catcher.BASE_WALK_SPEED, -Catcher.BASE_WALK_SPEED, Catcher.BASE_WALK_SPEED, -Catcher.BASE_WALK_SPEED,
})); }));
AddInternal(beatSnapGrid = new CatchBeatSnapGrid());
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -74,6 +78,29 @@ namespace osu.Game.Rulesets.Catch.Edit
inputManager = GetContainingInputManager(); inputManager = GetContainingInputManager();
} }
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
if (BlueprintContainer.CurrentTool is SelectTool)
{
if (EditorBeatmap.SelectedHitObjects.Any())
{
beatSnapGrid.SelectionTimeRange = (EditorBeatmap.SelectedHitObjects.Min(h => h.StartTime), EditorBeatmap.SelectedHitObjects.Max(h => h.GetEndTime()));
}
else
beatSnapGrid.SelectionTimeRange = null;
}
else
{
var result = FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position);
if (result.Time is double time)
beatSnapGrid.SelectionTimeRange = (time, time);
else
beatSnapGrid.SelectionTimeRange = null;
}
}
protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after) protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
{ {
// osu!catch's distance snap implementation is limited, in that a custom spacing cannot be specified. // osu!catch's distance snap implementation is limited, in that a custom spacing cannot be specified.
@ -132,7 +159,7 @@ namespace osu.Game.Rulesets.Catch.Edit
result.ScreenSpacePosition.X = screenSpacePosition.X; 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 && if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius) Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)

View File

@ -1,17 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Scoring namespace osu.Game.Rulesets.Catch.Scoring
{ {
public partial class CatchScoreProcessor : ScoreProcessor public partial class CatchScoreProcessor : ScoreProcessor
{ {
private const int combo_cap = 200;
private const double combo_base = 4;
public CatchScoreProcessor() public CatchScoreProcessor()
: base(new CatchRuleset()) : 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));
} }
} }

View File

@ -3,6 +3,7 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
@ -41,6 +42,8 @@ namespace osu.Game.Rulesets.Catch.UI
internal CatcherArea CatcherArea { get; private set; } = null!; internal CatcherArea CatcherArea { get; private set; } = null!;
public Container UnderlayElements { get; private set; } = null!;
private readonly IBeatmapDifficultyInfo difficulty; private readonly IBeatmapDifficultyInfo difficulty;
public CatchPlayfield(IBeatmapDifficultyInfo difficulty) public CatchPlayfield(IBeatmapDifficultyInfo difficulty)
@ -62,6 +65,10 @@ namespace osu.Game.Rulesets.Catch.UI
AddRangeInternal(new[] AddRangeInternal(new[]
{ {
UnderlayElements = new Container
{
RelativeSizeAxes = Axes.Both,
},
droppedObjectContainer, droppedObjectContainer,
Catcher.CreateProxiedContent(), Catcher.CreateProxiedContent(),
HitObjectContainer.CreateProxy(), HitObjectContainer.CreateProxy(),

View File

@ -21,18 +21,29 @@ namespace osu.Game.Rulesets.Mania.Configuration
{ {
base.InitialiseDefaults(); 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.ScrollDirection, ManiaScrollingDirection.Down);
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false); 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 public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
{ {
new TrackedSetting<double>(ManiaRulesetSetting.ScrollTime, new TrackedSetting<int>(ManiaRulesetSetting.ScrollSpeed,
scrollTime => new SettingDescription( speed => new SettingDescription(
rawValue: scrollTime, rawValue: speed,
name: RulesetSettingsStrings.ScrollSpeed, name: RulesetSettingsStrings.ScrollSpeed,
value: RulesetSettingsStrings.ScrollSpeedTooltip(scrollTime, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)) value: RulesetSettingsStrings.ScrollSpeedTooltip((int)DrawableManiaRuleset.ComputeScrollTime(speed), speed)
) )
) )
}; };
@ -40,7 +51,9 @@ namespace osu.Game.Rulesets.Mania.Configuration
public enum ManiaRulesetSetting public enum ManiaRulesetSetting
{ {
[Obsolete("Use ScrollSpeed instead.")] // Can be removed 2023-11-30
ScrollTime, ScrollTime,
ScrollSpeed,
ScrollDirection, ScrollDirection,
TimingBasedNoteColouring TimingBasedNoteColouring
} }

View File

@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Mania.Edit
} }
Color4 colour = BindableBeatDivisor.GetColourFor( Color4 colour = BindableBeatDivisor.GetColourFor(
BindableBeatDivisor.GetDivisorForBeatIndex(Math.Max(1, beat), beatDivisor.Value), colours); BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value), colours);
foreach (var grid in grids) foreach (var grid in grids)
{ {

View File

@ -389,41 +389,23 @@ namespace osu.Game.Rulesets.Mania
return base.GetDisplayNameForHitResult(result); return base.GetDisplayNameForHitResult(result);
} }
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
{
new StatisticRow
{
Columns = new[]
{ {
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y AutoSizeAxes = Axes.Y
}), }),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(score.HitEvents) new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(score.HitEvents)
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 250 Height = 250
}, true), }, true),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{ {
new AverageHitError(score.HitEvents), new AverageHitError(score.HitEvents),
new UnstableRate(score.HitEvents) new UnstableRate(score.HitEvents)
}), true) }), true)
}
}
}; };
public override IRulesetFilterCriteria CreateRulesetFilterCriteria() public override IRulesetFilterCriteria CreateRulesetFilterCriteria()

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
@ -34,10 +33,10 @@ namespace osu.Game.Rulesets.Mania
LabelText = RulesetSettingsStrings.ScrollingDirection, LabelText = RulesetSettingsStrings.ScrollingDirection,
Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection) Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection)
}, },
new SettingsSlider<double, ManiaScrollSlider> new SettingsSlider<int, ManiaScrollSlider>
{ {
LabelText = RulesetSettingsStrings.ScrollSpeed, LabelText = RulesetSettingsStrings.ScrollSpeed,
Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime), Current = config.GetBindable<int>(ManiaRulesetSetting.ScrollSpeed),
KeyboardStep = 5 KeyboardStep = 5
}, },
new SettingsCheckbox 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((int)DrawableManiaRuleset.ComputeScrollTime(Current.Value), Current.Value);
} }
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;

View File

@ -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: // 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. // 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. // 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. // How far past the hit target this hold note is.
float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y; float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Rulesets.Mania.Objects namespace osu.Game.Rulesets.Mania.Objects
{ {
public class HeadNote : Note public class HeadNote : Note

View File

@ -1,23 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // 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.Scoring;
namespace osu.Game.Rulesets.Mania.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() public ManiaScoreProcessor()
: base(new ManiaRuleset()) : base(new ManiaRuleset())
{ {
} }
protected override double DefaultAccuracyPortion => 0.99; protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{
return 10000 * comboProgress
+ 990000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
+ bonusPortion;
}
protected override double DefaultComboPortion => 0.01; 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));
protected override double ClassicScoreMultiplier => 16;
} }
} }

View File

@ -139,11 +139,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 3: case 3:
switch (columnIndex) switch (columnIndex)
{ {
case 0: return colour_pink; case 0: return colour_green;
case 1: return colour_orange; case 1: return colour_special_column;
case 2: return colour_yellow; case 2: return colour_cyan;
default: throw new ArgumentOutOfRangeException(); default: throw new ArgumentOutOfRangeException();
} }
@ -185,11 +185,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 1: return colour_orange; case 1: return colour_orange;
case 2: return colour_yellow; case 2: return colour_green;
case 3: return colour_cyan; case 3: return colour_cyan;
case 4: return colour_purple; case 4: return colour_orange;
case 5: return colour_pink; case 5: return colour_pink;
@ -201,17 +201,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{ {
case 0: return colour_pink; case 0: return colour_pink;
case 1: return colour_cyan; case 1: return colour_orange;
case 2: return colour_pink; case 2: return colour_pink;
case 3: return colour_special_column; case 3: return colour_special_column;
case 4: return colour_green; case 4: return colour_pink;
case 5: return colour_cyan; case 5: return colour_orange;
case 6: return colour_green; case 6: return colour_pink;
default: throw new ArgumentOutOfRangeException(); default: throw new ArgumentOutOfRangeException();
} }
@ -225,9 +225,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 2: return colour_orange; case 2: return colour_orange;
case 3: return colour_yellow; case 3: return colour_green;
case 4: return colour_yellow; case 4: return colour_cyan;
case 5: return colour_orange; case 5: return colour_orange;
@ -273,9 +273,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 3: return colour_yellow; case 3: return colour_yellow;
case 4: return colour_cyan; case 4: return colour_green;
case 5: return colour_green; case 5: return colour_cyan;
case 6: return colour_yellow; case 6: return colour_yellow;

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Pooling;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Extensions;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
@ -39,7 +40,11 @@ namespace osu.Game.Rulesets.Mania.UI
public readonly Bindable<ManiaAction> Action = new Bindable<ManiaAction>(); public readonly Bindable<ManiaAction> Action = new Bindable<ManiaAction>();
public readonly ColumnHitObjectArea HitObjectArea; public readonly ColumnHitObjectArea HitObjectArea;
internal readonly Container BackgroundContainer = new Container { RelativeSizeAxes = Axes.Both };
internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }; internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both };
private DrawablePool<PoolableHitExplosion> hitExplosionPool; private DrawablePool<PoolableHitExplosion> hitExplosionPool;
private readonly OrderedHitPolicy hitPolicy; private readonly OrderedHitPolicy hitPolicy;
public Container UnderlayElements => HitObjectArea.UnderlayElements; public Container UnderlayElements => HitObjectArea.UnderlayElements;
@ -76,30 +81,31 @@ namespace osu.Game.Rulesets.Mania.UI
skin.SourceChanged += onSourceChanged; skin.SourceChanged += onSourceChanged;
onSourceChanged(); onSourceChanged();
Drawable background = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground()) InternalChildren = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
};
InternalChildren = new[]
{ {
hitExplosionPool = new DrawablePool<PoolableHitExplosion>(5), hitExplosionPool = new DrawablePool<PoolableHitExplosion>(5),
sampleTriggerSource = new GameplaySampleTriggerSource(HitObjectContainer), sampleTriggerSource = new GameplaySampleTriggerSource(HitObjectContainer),
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
background.CreateProxy(),
HitObjectArea, HitObjectArea,
keyArea = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea()) keyArea = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
background, // For input purposes, the background is added at the highest depth, but is then proxied back below all other elements externally
// (see `Stage.columnBackgrounds`).
BackgroundContainer,
TopLevelContainer, TopLevelContainer,
new ColumnTouchInputArea(this) new ColumnTouchInputArea(this)
}; };
applyGameWideClock(background); var background = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
applyGameWideClock(keyArea); {
RelativeSizeAxes = Axes.Both,
};
background.ApplyGameWideClock(host);
keyArea.ApplyGameWideClock(host);
BackgroundContainer.Add(background);
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy()); TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
RegisterPool<Note, DrawableNote>(10, 50); RegisterPool<Note, DrawableNote>(10, 50);
@ -107,18 +113,6 @@ namespace osu.Game.Rulesets.Mania.UI
RegisterPool<HeadNote, DrawableHoldNoteHead>(10, 50); RegisterPool<HeadNote, DrawableHoldNoteHead>(10, 50);
RegisterPool<TailNote, DrawableHoldNoteTail>(10, 50); RegisterPool<TailNote, DrawableHoldNoteTail>(10, 50);
RegisterPool<HoldNoteTick, DrawableHoldNoteTick>(50, 250); RegisterPool<HoldNoteTick, DrawableHoldNoteTick>(50, 250);
// Some elements don't handle rewind correctly and fixing them is non-trivial.
// In the future we need a better solution to this, but as a temporary work-around, give these components the game-wide
// clock so they don't need to worry about rewind.
// This only works because they handle OnPressed/OnReleased which results in a correct state while rewinding.
//
// This is kinda dodgy (and will cause weirdness when pausing gameplay) but is better than completely broken rewind.
void applyGameWideClock(Drawable drawable)
{
drawable.Clock = host.UpdateThread.Clock;
drawable.ProcessCustomClock = false;
}
} }
private void onSourceChanged() private void onSourceChanged()

View File

@ -33,12 +33,12 @@ namespace osu.Game.Rulesets.Mania.UI
public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject> public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject>
{ {
/// <summary> /// <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> /// </summary>
public const double MIN_TIME_RANGE = 290; public const double MIN_TIME_RANGE = 290;
/// <summary> /// <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> /// </summary>
public const double MAX_TIME_RANGE = 11485; public const double MAX_TIME_RANGE = 11485;
@ -69,7 +69,8 @@ namespace osu.Game.Rulesets.Mania.UI
protected override ScrollVisualisationMethod VisualisationMethod => scrollMethod; protected override ScrollVisualisationMethod VisualisationMethod => scrollMethod;
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>(); 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. // Stores the current speed adjustment active in gameplay.
private readonly Track speedAdjustmentTrack = new TrackVirtual(0); private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
@ -78,6 +79,9 @@ namespace osu.Game.Rulesets.Mania.UI
: base(ruleset, beatmap, mods) : base(ruleset, beatmap, mods)
{ {
BarLines = new BarLineGenerator<BarLine>(Beatmap).BarLines; BarLines = new BarLineGenerator<BarLine>(Beatmap).BarLines;
TimeRange.MinValue = 1;
TimeRange.MaxValue = MAX_TIME_RANGE;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -104,30 +108,28 @@ namespace osu.Game.Rulesets.Mania.UI
Config.BindWith(ManiaRulesetSetting.ScrollDirection, configDirection); Config.BindWith(ManiaRulesetSetting.ScrollDirection, configDirection);
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true); configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
Config.BindWith(ManiaRulesetSetting.ScrollTime, configTimeRange); Config.BindWith(ManiaRulesetSetting.ScrollSpeed, configScrollSpeed);
TimeRange.MinValue = configTimeRange.MinValue; configScrollSpeed.BindValueChanged(speed => this.TransformTo(nameof(smoothTimeRange), ComputeScrollTime(speed.NewValue), 200, Easing.OutQuint));
TimeRange.MaxValue = configTimeRange.MaxValue;
TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value);
} }
protected override void AdjustScrollSpeed(int amount) protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += 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 Update() protected override void Update()
{ {
base.Update(); base.Update();
updateTimeRange(); 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(); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer();

View File

@ -60,6 +60,7 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X; AutoSizeAxes = Axes.X;
Container columnBackgrounds;
Container topLevelContainer; Container topLevelContainer;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
@ -77,9 +78,10 @@ namespace osu.Game.Rulesets.Mania.UI
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },
columnFlow = new ColumnFlow<Column>(definition) columnBackgrounds = new Container
{ {
RelativeSizeAxes = Axes.Y, Name = "Column backgrounds",
RelativeSizeAxes = Axes.Both,
}, },
new Container new Container
{ {
@ -98,6 +100,10 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
} }
}, },
columnFlow = new ColumnFlow<Column>(definition)
{
RelativeSizeAxes = Axes.Y,
},
new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground), _ => null) new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground), _ => null)
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
@ -126,6 +132,7 @@ namespace osu.Game.Rulesets.Mania.UI
}; };
topLevelContainer.Add(column.TopLevelContainer.CreateProxy()); topLevelContainer.Add(column.TopLevelContainer.CreateProxy());
columnBackgrounds.Add(column.BackgroundContainer.CreateProxy());
columnFlow.SetContentForColumn(i, column); columnFlow.SetContentForColumn(i, column);
AddNested(column); AddNested(column);
} }

View File

@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("place first object", () => InputManager.Click(MouseButton.Left)); AddStep("place first object", () => InputManager.Click(MouseButton.Left));
AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.02f, 0))); AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.01f, 0)));
AddStep("place second object", () => InputManager.Click(MouseButton.Left)); AddStep("place second object", () => InputManager.Click(MouseButton.Left));
@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("enter circle placement mode", () => InputManager.Key(Key.Number2)); AddStep("enter circle placement mode", () => InputManager.Key(Key.Number2));
AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.235f, 0))); AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.205f, 0)));
AddStep("place second object", () => InputManager.Click(MouseButton.Left)); AddStep("place second object", () => InputManager.Click(MouseButton.Left));
@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.02f, 0))); AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.01f, 0)));
AddAssert("object 3 snapped to 1", () => AddAssert("object 3 snapped to 1", () =>
{ {
@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
return Precision.AlmostEquals(first.EndPosition, third.Position); return Precision.AlmostEquals(first.EndPosition, third.Position);
}); });
AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * -0.22f, playfield.ScreenSpaceDrawQuad.Width * 0.21f))); AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * -0.21f, playfield.ScreenSpaceDrawQuad.Width * 0.205f)));
AddAssert("object 2 snapped to 1", () => AddAssert("object 2 snapped to 1", () =>
{ {

View File

@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
if (slider is null) return; 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()); slider.Samples.Add(sample.With());
}); });

View File

@ -17,6 +17,18 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{ {
public partial class TestSceneOsuModAutoplay : OsuModTestScene 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] [Test]
public void TestSpmUnaffectedByRateAdjust() public void TestSpmUnaffectedByRateAdjust()
=> runSpmTest(new OsuModDaycore => runSpmTest(new OsuModDaycore

View File

@ -309,7 +309,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
} }
else 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; Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? Parent.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;

View File

@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
} }
// Update the cursor position. // 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; cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
} }
else if (cursor != null) else if (cursor != null)

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{ {
public enum SliderPosition public enum SliderPosition

View File

@ -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. // 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 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. // 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)); (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)) 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); SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
if (snapType.HasFlagFast(SnapType.Grids)) if (snapType.HasFlagFast(SnapType.RelativeGrids))
{ {
if (DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) if (DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{ {
@ -164,7 +164,10 @@ namespace osu.Game.Rulesets.Osu.Edit
result.ScreenSpacePosition = distanceSnapGrid.ToScreenSpace(pos); result.ScreenSpacePosition = distanceSnapGrid.ToScreenSpace(pos);
result.Time = time; result.Time = time;
} }
}
if (snapType.HasFlagFast(SnapType.GlobalGrids))
{
if (rectangularGridSnapToggle.Value == TernaryState.True) if (rectangularGridSnapToggle.Value == TernaryState.True)
{ {
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(result.ScreenSpacePosition)); Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));
@ -184,7 +187,7 @@ namespace osu.Game.Rulesets.Osu.Edit
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
float snapRadius = float snapRadius =
playfield.GamefieldToScreenSpace(new Vector2(OsuHitObject.OBJECT_RADIUS / 5)).X - playfield.GamefieldToScreenSpace(new Vector2(OsuHitObject.OBJECT_RADIUS * 0.10f)).X -
playfield.GamefieldToScreenSpace(Vector2.Zero).X; playfield.GamefieldToScreenSpace(Vector2.Zero).X;
foreach (var b in blueprints) foreach (var b in blueprints)

View File

@ -24,7 +24,17 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation; public override ModType Type => ModType.Automation;
public override LocalisableString Description => @"Automatic cursor movement - just follow the rhythm."; public override LocalisableString Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 0.1; 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; public bool PerformFail() => false;
@ -34,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Mods
private List<OsuReplayFrame> replayFrames = null!; private List<OsuReplayFrame> replayFrames = null!;
private int currentFrame; private int currentFrame = -1;
public void Update(Playfield playfield) public void Update(Playfield playfield)
{ {
@ -43,8 +53,9 @@ namespace osu.Game.Rulesets.Osu.Mods
double time = playfield.Clock.CurrentTime; double time = playfield.Clock.CurrentTime;
// Very naive implementation of autopilot based on proximity to replay frames. // 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). // 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++; currentFrame++;
new MousePositionAbsoluteInput { Position = playfield.ToScreenSpace(replayFrames[currentFrame].Position) }.Apply(inputManager.CurrentState, inputManager); new MousePositionAbsoluteInput { Position = playfield.ToScreenSpace(replayFrames[currentFrame].Position) }.Apply(inputManager.CurrentState, inputManager);

View File

@ -42,14 +42,14 @@ namespace osu.Game.Rulesets.Osu.Mods
private PlayfieldAdjustmentContainer bubbleContainer = null!; private PlayfieldAdjustmentContainer bubbleContainer = null!;
private DrawablePool<BubbleDrawable> bubblePool = null!;
private readonly Bindable<int> currentCombo = new BindableInt(); private readonly Bindable<int> currentCombo = new BindableInt();
private float maxSize; private float maxSize;
private float bubbleSize; private float bubbleSize;
private double bubbleFade; private double bubbleFade;
private readonly DrawablePool<BubbleDrawable> bubblePool = new DrawablePool<BubbleDrawable>(100);
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
@ -72,6 +72,7 @@ namespace osu.Game.Rulesets.Osu.Mods
bubbleContainer = drawableRuleset.CreatePlayfieldAdjustmentContainer(); bubbleContainer = drawableRuleset.CreatePlayfieldAdjustmentContainer();
drawableRuleset.Overlays.Add(bubbleContainer); drawableRuleset.Overlays.Add(bubbleContainer);
drawableRuleset.Overlays.Add(bubblePool = new DrawablePool<BubbleDrawable>(100));
} }
public void ApplyToDrawableHitObject(DrawableHitObject drawableObject) public void ApplyToDrawableHitObject(DrawableHitObject drawableObject)

View File

@ -18,7 +18,7 @@ using static osu.Game.Input.Handlers.ReplayInputHandler;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer, IHasNoTimedInputs
{ {
public override LocalisableString Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; public override LocalisableString Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public interface IRequireTracking public interface IRequireTracking

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Rulesets.Osu.Objects namespace osu.Game.Rulesets.Osu.Objects
{ {
public interface ISliderProgress public interface ISliderProgress

View File

@ -291,56 +291,32 @@ namespace osu.Game.Rulesets.Osu
return base.GetDisplayNameForHitResult(result); return base.GetDisplayNameForHitResult(result);
} }
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{ {
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList(); var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();
return new[] return new[]
{
new StatisticRow
{
Columns = new[]
{ {
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y AutoSizeAxes = Axes.Y
}), }),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents) new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents)
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 250 Height = 250
}, true), }, true),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem("Accuracy Heatmap", () => new AccuracyHeatmap(score, playableBeatmap) new StatisticItem("Accuracy Heatmap", () => new AccuracyHeatmap(score, playableBeatmap)
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 250 Height = 250
}, true), }, true),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{ {
new AverageHitError(timedHitEvents), new AverageHitError(timedHitEvents),
new UnstableRate(timedHitEvents) new UnstableRate(timedHitEvents)
}), true) }), true)
}
}
}; };
} }

View File

@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // 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.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.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) protected override HitEvent CreateHitEvent(JudgementResult result)
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit); => 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) return 700000 * comboProgress
{ + 300000 * Math.Pow(Accuracy.Value, 10) * accuracyProgress
case HitCircle: + bonusPortion;
return new OsuHitCircleJudgementResult(hitObject, judgement);
default:
return new OsuJudgementResult(hitObject, judgement);
}
} }
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@ -13,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Scoring; using osu.Game.Scoring;
using osuTK; using osuTK;
@ -36,8 +35,8 @@ namespace osu.Game.Rulesets.Osu.Statistics
private const float rotation = 45; private const float rotation = 45;
private BufferedContainer bufferedGrid; private BufferedContainer bufferedGrid = null!;
private GridContainer pointGrid; private GridContainer pointGrid = null!;
private readonly ScoreInfo score; private readonly ScoreInfo score;
private readonly IBeatmap playableBeatmap; private readonly IBeatmap playableBeatmap;
@ -58,6 +57,8 @@ namespace osu.Game.Rulesets.Osu.Statistics
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
const float line_extension = 0.2f;
InternalChild = new Container InternalChild = new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -65,6 +66,11 @@ namespace osu.Game.Rulesets.Osu.Statistics
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit, FillMode = FillMode.Fit,
Children = new Drawable[] Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{ {
new CircularContainer new CircularContainer
{ {
@ -81,63 +87,81 @@ namespace osu.Game.Rulesets.Osu.Statistics
Colour = Color4Extensions.FromHex("#202624") Colour = Color4Extensions.FromHex("#202624")
} }
}, },
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(1), Padding = new MarginPadding(1),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Rotation = rotation,
Child = new Container Child = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[] Children = new Drawable[]
{ {
new Box new Circle
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
EdgeSmoothness = new Vector2(1),
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Height = 2, // We're rotating along a diagonal - we don't really care how big this is. Width = line_thickness,
Width = line_thickness / 2, Height = inner_portion + line_extension,
Rotation = -rotation, Rotation = -rotation * 2,
Alpha = 0.3f, Alpha = 0.6f,
}, },
new Box new Circle
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
EdgeSmoothness = new Vector2(1),
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Height = 2, // We're rotating along a diagonal - we don't really care how big this is. Width = line_thickness,
Width = line_thickness / 2, // adjust for edgesmoothness Height = inner_portion + line_extension,
Rotation = rotation
}, },
} new OsuSpriteText
},
},
new Box
{ {
Anchor = Anchor.TopRight, Text = "Overshoot",
Origin = Anchor.TopRight, Anchor = Anchor.Centre,
Width = 10, Origin = Anchor.BottomCentre,
EdgeSmoothness = new Vector2(1), Padding = new MarginPadding(3),
Height = line_thickness / 2, // adjust for edgesmoothness RelativePositionAxes = Axes.Both,
Y = -(inner_portion + line_extension) / 2,
}, },
new Box new OsuSpriteText
{ {
Anchor = Anchor.TopRight, Text = "Undershoot",
Origin = Anchor.TopRight, Anchor = Anchor.Centre,
EdgeSmoothness = new Vector2(1), Origin = Anchor.TopCentre,
Width = line_thickness / 2, // adjust for edgesmoothness 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, 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,
} }
} }
}, },
},
}
},
bufferedGrid = new BufferedContainer(cachedFrameBuffer: true) bufferedGrid = new BufferedContainer(cachedFrameBuffer: true)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,

View File

@ -15,187 +15,175 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
[Test] [Test]
public void TestHitAllDrumRoll() public void TestHitAllDrumRoll()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
new TaikoReplayFrame(1000, TaikoAction.LeftCentre), new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1001), new TaikoReplayFrame(1001),
new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
new TaikoReplayFrame(1251),
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
new TaikoReplayFrame(1501),
new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
new TaikoReplayFrame(1751),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre), new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001), new TaikoReplayFrame(2001),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(false)));
{
StartTime = hit_time,
Duration = 1000
}));
AssertJudgementCount(3); AssertJudgementCount(6);
AssertResult<DrumRollTick>(0, HitResult.SmallBonus); AssertResult<DrumRollTick>(0, HitResult.SmallBonus);
AssertResult<DrumRollTick>(1, HitResult.SmallBonus); AssertResult<DrumRollTick>(1, HitResult.SmallBonus);
AssertResult<DrumRollTick>(2, HitResult.SmallBonus);
AssertResult<DrumRollTick>(3, HitResult.SmallBonus);
AssertResult<DrumRollTick>(4, HitResult.SmallBonus);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
} }
[Test] [Test]
public void TestHitSomeDrumRoll() public void TestHitSomeDrumRoll()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre), new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001), new TaikoReplayFrame(2001),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(false)));
{
StartTime = hit_time,
Duration = 1000
}));
AssertJudgementCount(3); AssertJudgementCount(6);
AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss); AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(1, HitResult.SmallBonus); AssertResult<DrumRollTick>(1, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(2, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(3, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(4, HitResult.SmallBonus);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
} }
[Test] [Test]
public void TestHitNoneDrumRoll() public void TestHitNoneDrumRoll()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(false)));
{
StartTime = hit_time,
Duration = 1000
}));
AssertJudgementCount(3); AssertJudgementCount(6);
AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss); AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(1, HitResult.IgnoreMiss); AssertResult<DrumRollTick>(1, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(2, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(3, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(4, HitResult.IgnoreMiss);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
} }
[Test] [Test]
public void TestHitAllStrongDrumRollWithOneKey() public void TestHitAllStrongDrumRollWithOneKey()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
new TaikoReplayFrame(1000, TaikoAction.LeftCentre), new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1001), new TaikoReplayFrame(1001),
new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
new TaikoReplayFrame(1251),
new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
new TaikoReplayFrame(1501),
new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
new TaikoReplayFrame(1751),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre), new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001), new TaikoReplayFrame(2001),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(true)));
AssertJudgementCount(12);
for (int i = 0; i < 5; i++)
{ {
StartTime = hit_time, AssertResult<DrumRollTick>(i, HitResult.SmallBonus);
Duration = 1000, AssertResult<StrongNestedHitObject>(i, HitResult.LargeBonus);
IsStrong = true }
}));
AssertJudgementCount(6);
AssertResult<DrumRollTick>(0, HitResult.SmallBonus);
AssertResult<StrongNestedHitObject>(0, HitResult.LargeBonus);
AssertResult<DrumRollTick>(1, HitResult.SmallBonus);
AssertResult<StrongNestedHitObject>(1, HitResult.LargeBonus);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
AssertResult<StrongNestedHitObject>(2, HitResult.IgnoreHit); AssertResult<StrongNestedHitObject>(5, HitResult.IgnoreHit);
} }
[Test] [Test]
public void TestHitSomeStrongDrumRollWithOneKey() public void TestHitSomeStrongDrumRollWithOneKey()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre), new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001), new TaikoReplayFrame(2001),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(true)));
{
StartTime = hit_time,
Duration = 1000,
IsStrong = true
}));
AssertJudgementCount(6); AssertJudgementCount(12);
AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss); AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss);
AssertResult<StrongNestedHitObject>(0, HitResult.IgnoreMiss); AssertResult<StrongNestedHitObject>(0, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(1, HitResult.SmallBonus); AssertResult<DrumRollTick>(4, HitResult.SmallBonus);
AssertResult<StrongNestedHitObject>(1, HitResult.LargeBonus); AssertResult<StrongNestedHitObject>(4, HitResult.LargeBonus);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
AssertResult<StrongNestedHitObject>(2, HitResult.IgnoreHit); AssertResult<StrongNestedHitObject>(5, HitResult.IgnoreHit);
} }
[Test] [Test]
public void TestHitAllStrongDrumRollWithBothKeys() public void TestHitAllStrongDrumRollWithBothKeys()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
new TaikoReplayFrame(1000, TaikoAction.LeftCentre, TaikoAction.RightCentre), new TaikoReplayFrame(1000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
new TaikoReplayFrame(1001), new TaikoReplayFrame(1001),
new TaikoReplayFrame(1250, TaikoAction.LeftCentre, TaikoAction.RightCentre),
new TaikoReplayFrame(1251),
new TaikoReplayFrame(1500, TaikoAction.LeftCentre, TaikoAction.RightCentre),
new TaikoReplayFrame(1501),
new TaikoReplayFrame(1750, TaikoAction.LeftCentre, TaikoAction.RightCentre),
new TaikoReplayFrame(1751),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre, TaikoAction.RightCentre), new TaikoReplayFrame(2000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
new TaikoReplayFrame(2001), new TaikoReplayFrame(2001),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(true)));
AssertJudgementCount(12);
for (int i = 0; i < 5; i++)
{ {
StartTime = hit_time, AssertResult<DrumRollTick>(i, HitResult.SmallBonus);
Duration = 1000, AssertResult<StrongNestedHitObject>(i, HitResult.LargeBonus);
IsStrong = true }
}));
AssertJudgementCount(6);
AssertResult<DrumRollTick>(0, HitResult.SmallBonus);
AssertResult<StrongNestedHitObject>(0, HitResult.LargeBonus);
AssertResult<DrumRollTick>(1, HitResult.SmallBonus);
AssertResult<StrongNestedHitObject>(1, HitResult.LargeBonus);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
AssertResult<StrongNestedHitObject>(2, HitResult.IgnoreHit); AssertResult<StrongNestedHitObject>(5, HitResult.IgnoreHit);
} }
[Test] [Test]
public void TestHitSomeStrongDrumRollWithBothKeys() public void TestHitSomeStrongDrumRollWithBothKeys()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre, TaikoAction.RightCentre), new TaikoReplayFrame(2000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
new TaikoReplayFrame(2001), new TaikoReplayFrame(2001),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(true)));
{
StartTime = hit_time,
Duration = 1000,
IsStrong = true
}));
AssertJudgementCount(6); AssertJudgementCount(12);
AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss); AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss);
AssertResult<StrongNestedHitObject>(0, HitResult.IgnoreMiss); AssertResult<StrongNestedHitObject>(0, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(1, HitResult.SmallBonus); AssertResult<DrumRollTick>(4, HitResult.SmallBonus);
AssertResult<StrongNestedHitObject>(1, HitResult.LargeBonus); AssertResult<StrongNestedHitObject>(4, HitResult.LargeBonus);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
AssertResult<StrongNestedHitObject>(2, HitResult.IgnoreHit); AssertResult<StrongNestedHitObject>(5, HitResult.IgnoreHit);
} }
private DrumRoll createDrumRoll(bool strong) => new DrumRoll
{
StartTime = 1000,
Duration = 1000,
IsStrong = strong
};
} }
} }

View File

@ -91,8 +91,9 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{ {
prepareDrawableRulesetAndBeatmap(false); prepareDrawableRulesetAndBeatmap(false);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); var hit = new Hit();
assertStateAfterResult(new JudgementResult(new Hit.StrongNestedHit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle); 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] [Test]

View File

@ -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();
}
}
}

View File

@ -92,6 +92,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
}).ToList(); }).ToList();
} }
// TODO: stable makes the last tick of a drumroll non-required when the next object is too close.
// This probably needs to be reimplemented:
//
// List<HitObject> hitobjects = hitObjectManager.hitObjects;
// int ind = hitobjects.IndexOf(this);
// if (i < hitobjects.Count - 1 && hitobjects[i + 1].HittableStartTime - (EndTime + (int)TickSpacing) <= (int)TickSpacing)
// lastTickHittable = false;
return converted; return converted;
} }
@ -133,7 +141,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
StartTime = obj.StartTime, StartTime = obj.StartTime,
Samples = obj.Samples, Samples = obj.Samples,
Duration = taikoDuration, Duration = taikoDuration,
TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4,
SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1 SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1
}; };
} }

View File

@ -118,6 +118,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{ {
public override bool RemoveWhenNotAlive => false; 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 public abstract partial class DrawableTaikoHitObject<TObject> : DrawableTaikoHitObject
@ -157,9 +160,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
Content.Add(MainPiece = CreateMainPiece()); 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(); protected abstract SkinnableDrawable CreateMainPiece();
} }
} }

View File

@ -1,13 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using System.Threading; using System.Threading;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
@ -71,6 +67,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
double scoringDistance = base_distance * difficulty.SliderMultiplier * SliderVelocity; double scoringDistance = base_distance * difficulty.SliderMultiplier * SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength; Velocity = scoringDistance / timingPoint.BeatLength;
TickRate = difficulty.SliderTickRate == 3 ? 3 : 4;
tickSpacing = timingPoint.BeatLength / TickRate; tickSpacing = timingPoint.BeatLength / TickRate;
} }
@ -98,7 +96,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
TickSpacing = tickSpacing, TickSpacing = tickSpacing,
StartTime = t, StartTime = t,
IsStrong = IsStrong, IsStrong = IsStrong,
Samples = Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToList() Samples = Samples
}); });
first = false; first = false;
@ -109,12 +107,21 @@ namespace osu.Game.Rulesets.Taiko.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty; 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 public class StrongNestedHit : StrongNestedHitObject
{ {
// The strong hit of the drum roll doesn't actually provide any score. // The strong hit of the drum roll doesn't actually provide any score.
public override Judgement CreateJudgement() => new IgnoreJudgement(); public override Judgement CreateJudgement() => new IgnoreJudgement();
public StrongNestedHit(TaikoHitObject parent)
: base(parent)
{
}
} }
#region LegacyBeatmapEncoder #region LegacyBeatmapEncoder

View File

@ -33,10 +33,18 @@ namespace osu.Game.Rulesets.Taiko.Objects
public override double MaximumJudgementOffset => HitWindow; 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 class StrongNestedHit : StrongNestedHitObject
{ {
public StrongNestedHit(TaikoHitObject parent)
: base(parent)
{
}
} }
} }
} }

View File

@ -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 class StrongNestedHit : StrongNestedHitObject
{ {
public StrongNestedHit(TaikoHitObject parent)
: base(parent)
{
}
} }
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Rulesets.Taiko.Objects namespace osu.Game.Rulesets.Taiko.Objects
{ {
/// <summary> /// <summary>

View File

@ -15,6 +15,13 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// </summary> /// </summary>
public abstract class StrongNestedHitObject : TaikoHitObject public abstract class StrongNestedHitObject : TaikoHitObject
{ {
public readonly TaikoHitObject Parent;
protected StrongNestedHitObject(TaikoHitObject parent)
{
Parent = parent;
}
public override Judgement CreateJudgement() => new TaikoStrongJudgement(); public override Judgement CreateJudgement() => new TaikoStrongJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;

View File

@ -33,7 +33,10 @@ namespace osu.Game.Rulesets.Taiko.Objects
for (int i = 0; i < RequiredHits; i++) for (int i = 0; i < RequiredHits; i++)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
AddNested(new SwellTick()); AddNested(new SwellTick
{
Samples = Samples
});
} }
} }

View File

@ -1,23 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // 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.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Scoring namespace osu.Game.Rulesets.Taiko.Scoring
{ {
internal partial class TaikoScoreProcessor : ScoreProcessor public partial class TaikoScoreProcessor : ScoreProcessor
{ {
private const double combo_base = 4;
public TaikoScoreProcessor() public TaikoScoreProcessor()
: base(new TaikoRuleset()) : 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;
}
} }
} }

View File

@ -1,9 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -18,7 +16,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
public partial class ArgonHitExplosion : CompositeDrawable, IAnimatableHitExplosion public partial class ArgonHitExplosion : CompositeDrawable, IAnimatableHitExplosion
{ {
private readonly TaikoSkinComponents component; private readonly TaikoSkinComponents component;
private readonly Circle outer; private readonly Circle outer;
private readonly Circle inner;
public ArgonHitExplosion(TaikoSkinComponents component) public ArgonHitExplosion(TaikoSkinComponents component)
{ {
@ -34,13 +34,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(
new Color4(255, 227, 236, 255),
new Color4(255, 198, 211, 255)
),
Masking = true, Masking = true,
}, },
new Circle inner = new Circle
{ {
Name = "Inner circle", Name = "Inner circle",
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -48,12 +44,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.White, Colour = Color4.White,
Size = new Vector2(0.85f), Size = new Vector2(0.85f),
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = new Color4(255, 132, 191, 255).Opacity(0.5f),
Radius = 45,
},
Masking = true, Masking = true,
}, },
}; };
@ -63,6 +53,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{ {
this.FadeOut(); this.FadeOut();
bool isRim = (drawableHitObject.HitObject as Hit)?.Type == HitType.Rim;
outer.Colour = isRim ? ArgonInputDrum.RIM_HIT_GRADIENT : ArgonInputDrum.CENTRE_HIT_GRADIENT;
inner.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = (isRim ? ArgonInputDrum.RIM_HIT_GLOW : ArgonInputDrum.CENTRE_HIT_GLOW).Opacity(0.5f),
Radius = 45,
};
switch (component) switch (component)
{ {
case TaikoSkinComponents.TaikoExplosionGreat: case TaikoSkinComponents.TaikoExplosionGreat:

View File

@ -19,6 +19,20 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{ {
public partial class ArgonInputDrum : AspectContainer public partial class ArgonInputDrum : AspectContainer
{ {
public static readonly ColourInfo RIM_HIT_GRADIENT = ColourInfo.GradientHorizontal(
new Color4(227, 248, 255, 255),
new Color4(198, 245, 255, 255)
);
public static readonly Colour4 RIM_HIT_GLOW = new Color4(126, 215, 253, 255);
public static readonly ColourInfo CENTRE_HIT_GRADIENT = ColourInfo.GradientHorizontal(
new Color4(255, 227, 236, 255),
new Color4(255, 198, 211, 255)
);
public static readonly Colour4 CENTRE_HIT_GLOW = new Color4(255, 147, 199, 255);
private const float rim_size = 0.3f; private const float rim_size = 0.3f;
public ArgonInputDrum() public ArgonInputDrum()
@ -141,14 +155,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
Anchor = anchor, Anchor = anchor,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal( Colour = RIM_HIT_GRADIENT,
new Color4(227, 248, 255, 255),
new Color4(198, 245, 255, 255)
),
EdgeEffect = new EdgeEffectParameters EdgeEffect = new EdgeEffectParameters
{ {
Type = EdgeEffectType.Glow, Type = EdgeEffectType.Glow,
Colour = new Color4(126, 215, 253, 170), Colour = RIM_HIT_GLOW.Opacity(0.66f),
Radius = 50, Radius = 50,
}, },
Alpha = 0, Alpha = 0,
@ -166,14 +177,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
Anchor = anchor, Anchor = anchor,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal( Colour = CENTRE_HIT_GRADIENT,
new Color4(255, 227, 236, 255),
new Color4(255, 198, 211, 255)
),
EdgeEffect = new EdgeEffectParameters EdgeEffect = new EdgeEffectParameters
{ {
Type = EdgeEffectType.Glow, Type = EdgeEffectType.Glow,
Colour = new Color4(255, 147, 199, 255), Colour = CENTRE_HIT_GLOW,
Radius = 50, Radius = 50,
}, },
Size = new Vector2(1 - rim_size), Size = new Vector2(1 - rim_size),

View File

@ -229,45 +229,27 @@ namespace osu.Game.Rulesets.Taiko
return base.GetDisplayNameForHitResult(result); return base.GetDisplayNameForHitResult(result);
} }
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{ {
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList(); var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList();
return new[] return new[]
{
new StatisticRow
{
Columns = new[]
{ {
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y AutoSizeAxes = Axes.Y
}), }),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents) new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents)
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 250 Height = 250
}, true), }, true),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{ {
new AverageHitError(timedHitEvents), new AverageHitError(timedHitEvents),
new UnstableRate(timedHitEvents) new UnstableRate(timedHitEvents)
}), true) }), true)
}
}
}; };
} }
} }

View File

@ -253,17 +253,17 @@ namespace osu.Game.Tests.Beatmaps.Formats
var soundPoint = controlPoints.SamplePointAt(0); var soundPoint = controlPoints.SamplePointAt(0);
Assert.AreEqual(956, soundPoint.Time); Assert.AreEqual(956, soundPoint.Time);
Assert.AreEqual("soft", soundPoint.SampleBank); Assert.AreEqual(HitSampleInfo.BANK_SOFT, soundPoint.SampleBank);
Assert.AreEqual(60, soundPoint.SampleVolume); Assert.AreEqual(60, soundPoint.SampleVolume);
soundPoint = controlPoints.SamplePointAt(53373); soundPoint = controlPoints.SamplePointAt(53373);
Assert.AreEqual(53373, soundPoint.Time); Assert.AreEqual(53373, soundPoint.Time);
Assert.AreEqual("soft", soundPoint.SampleBank); Assert.AreEqual(HitSampleInfo.BANK_SOFT, soundPoint.SampleBank);
Assert.AreEqual(60, soundPoint.SampleVolume); Assert.AreEqual(60, soundPoint.SampleVolume);
soundPoint = controlPoints.SamplePointAt(119637); soundPoint = controlPoints.SamplePointAt(119637);
Assert.AreEqual(119637, soundPoint.Time); Assert.AreEqual(119637, soundPoint.Time);
Assert.AreEqual("soft", soundPoint.SampleBank); Assert.AreEqual(HitSampleInfo.BANK_SOFT, soundPoint.SampleBank);
Assert.AreEqual(80, soundPoint.SampleVolume); Assert.AreEqual(80, soundPoint.SampleVolume);
var effectPoint = controlPoints.EffectPointAt(0); 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(2500).KiaiMode, Is.False);
Assert.That(controlPoints.EffectPointAt(3500).KiaiMode, Is.True); Assert.That(controlPoints.EffectPointAt(3500).KiaiMode, Is.True);
Assert.That(controlPoints.SamplePointAt(500).SampleBank, Is.EqualTo("drum")); Assert.That(controlPoints.SamplePointAt(500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_DRUM));
Assert.That(controlPoints.SamplePointAt(1500).SampleBank, Is.EqualTo("drum")); Assert.That(controlPoints.SamplePointAt(1500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_DRUM));
Assert.That(controlPoints.SamplePointAt(2500).SampleBank, Is.EqualTo("normal")); Assert.That(controlPoints.SamplePointAt(2500).SampleBank, Is.EqualTo(HitSampleInfo.BANK_NORMAL));
Assert.That(controlPoints.SamplePointAt(3500).SampleBank, Is.EqualTo("drum")); 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(500).BeatLength, Is.EqualTo(500).Within(0.1));
Assert.That(controlPoints.TimingPointAt(1500).BeatLength, Is.EqualTo(500).Within(0.1)); Assert.That(controlPoints.TimingPointAt(1500).BeatLength, Is.EqualTo(500).Within(0.1));

View File

@ -231,7 +231,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
protected override IBeatmap GetBeatmap() => beatmap; protected override IBeatmap GetBeatmap() => beatmap;
protected override Texture GetBackground() => throw new NotImplementedException(); public override Texture GetBackground() => throw new NotImplementedException();
protected override Track GetBeatmapTrack() => throw new NotImplementedException(); protected override Track GetBeatmapTrack() => throw new NotImplementedException();

View File

@ -131,7 +131,7 @@ namespace osu.Game.Tests.Editing.Checks
var mock = new Mock<IWorkingBeatmap>(); var mock = new Mock<IWorkingBeatmap>();
mock.SetupGet(w => w.Beatmap).Returns(beatmap); mock.SetupGet(w => w.Beatmap).Returns(beatmap);
mock.SetupGet(w => w.Background).Returns(background); mock.Setup(w => w.GetBackground()).Returns(background);
mock.Setup(w => w.GetStream(It.IsAny<string>())).Returns(stream); mock.Setup(w => w.GetStream(It.IsAny<string>())).Returns(stream);
return mock; return mock;

View File

@ -76,22 +76,38 @@ namespace osu.Game.Tests.Gameplay
// Reset with a miss instead. // Reset with a miss instead.
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame 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.TotalScore.Value, Is.Zero);
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1)); Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0)); Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(0));
// Reset with no judged hit. // Reset with no judged hit.
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame 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.TotalScore.Value, Is.Zero);
Assert.That(scoreProcessor.JudgedHits, Is.Zero); Assert.That(scoreProcessor.JudgedHits, Is.Zero);
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0)); Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1));
} }
[Test] [Test]

View File

@ -179,7 +179,7 @@ namespace osu.Game.Tests.Resources
BeatmapHash = beatmap.Hash, BeatmapHash = beatmap.Hash,
Ruleset = beatmap.Ruleset, Ruleset = beatmap.Ruleset,
Mods = new Mod[] { new TestModHardRock(), new TestModDoubleTime() }, Mods = new Mod[] { new TestModHardRock(), new TestModDoubleTime() },
TotalScore = 2845370, TotalScore = 284537,
Accuracy = 0.95, Accuracy = 0.95,
MaxCombo = 999, MaxCombo = 999,
Position = 1, Position = 1,

View 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:

View File

@ -14,11 +14,12 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Scoring; using osu.Game.Scoring.Legacy;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Rulesets.Scoring namespace osu.Game.Tests.Rulesets.Scoring
@ -31,7 +32,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
scoreProcessor = new ScoreProcessor(new TestRuleset()); scoreProcessor = new ScoreProcessor(new OsuRuleset());
beatmap = new TestBeatmap(new RulesetInfo()) beatmap = new TestBeatmap(new RulesetInfo())
{ {
HitObjects = new List<HitObject> 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.Meh, 116_667)]
[TestCase(ScoringMode.Standardised, HitResult.Ok, 800_000)] [TestCase(ScoringMode.Standardised, HitResult.Ok, 233_338)]
[TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)] [TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)]
[TestCase(ScoringMode.Classic, HitResult.Meh, 20)] [TestCase(ScoringMode.Classic, HitResult.Meh, 0)]
[TestCase(ScoringMode.Classic, HitResult.Ok, 23)] [TestCase(ScoringMode.Classic, HitResult.Ok, 2)]
[TestCase(ScoringMode.Classic, HitResult.Great, 36)] [TestCase(ScoringMode.Classic, HitResult.Great, 36)]
public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore) public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
{ {
scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(beatmap); scoreProcessor.ApplyBeatmap(beatmap);
var judgementResult = new JudgementResult(beatmap.HitObjects.Single(), new OsuJudgement()) var judgementResult = new JudgementResult(beatmap.HitObjects.Single(), new OsuJudgement())
@ -58,7 +58,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
}; };
scoreProcessor.ApplyResult(judgementResult); 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> /// <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> /// <param name="expectedScore">Expected score after all objects have been judged, rounded to the nearest integer.</param>
/// <remarks> /// <remarks>
/// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and 50% max combo. /// 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> /// </remarks>
[TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] // (3 * 0) / (4 * 300) * 300_000 + (0 / 4) * 700_000 [TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)]
[TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 387_500)] // (3 * 50) / (4 * 300) * 300_000 + (2 / 4) * 700_000 [TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 79_333)]
[TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 425_000)] // (3 * 100) / (4 * 300) * 300_000 + (2 / 4) * 700_000 [TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 158_667)]
[TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 492_857)] // (3 * 200) / (4 * 350) * 300_000 + (2 / 4) * 700_000 [TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 302_402)]
[TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 575_000)] // (3 * 300) / (4 * 300) * 300_000 + (2 / 4) * 700_000 [TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 492_894)]
[TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 575_000)] // (3 * 350) / (4 * 350) * 300_000 + (2 / 4) * 700_000 [TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 492_894)]
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 700_000)] // (3 * 0) / (4 * 10) * 300_000 + 700_000 (max combo 0) [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 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.SmallTickHit, HitResult.SmallTickHit, 541_894)]
[TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (3 * 0) / (4 * 30) * 300_000 + (0 / 4) * 700_000 [TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
[TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] // (3 * 30) / (4 * 30) * 300_000 + (0 / 4) * 700_000 [TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 492_894)]
[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.SmallBonus, HitResult.SmallBonus, 1_000_030)]
[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.LargeBonus, HitResult.LargeBonus, 1_000_150)]
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] [TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)]
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 86)] [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 4)]
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 104)] [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 15)]
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 140)] [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 53)]
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 190)] [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 140)]
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 190)] [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 140)]
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 18)] [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)]
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 31)] [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 11)]
[TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] [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.SmallBonus, HitResult.SmallBonus, 36)]
[TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 36)] [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 36)]
public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore) 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)) HitObjects = new List<HitObject>(Enumerable.Repeat(new TestHitObject(maxResult), 4))
}; };
scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(fourObjectBeatmap); scoreProcessor.ApplyBeatmap(fourObjectBeatmap);
for (int i = 0; i < 4; i++) 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 Type = i == 2 ? minResult : hitResult
}; };
scoreProcessor.ApplyResult(judgementResult); 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));
}
/// <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));
} }
[Test] [Test]
@ -173,10 +122,9 @@ namespace osu.Game.Tests.Rulesets.Scoring
[Values(ScoringMode.Standardised, ScoringMode.Classic)] [Values(ScoringMode.Standardised, ScoringMode.Classic)]
ScoringMode scoringMode) ScoringMode scoringMode)
{ {
scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(new TestBeatmap(new RulesetInfo())); 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)] [TestCase(HitResult.IgnoreHit, HitResult.IgnoreMiss)]
@ -294,28 +242,6 @@ namespace osu.Game.Tests.Rulesets.Scoring
Assert.AreEqual(expectedReturnValue, hitResult.IsScorable()); 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 #pragma warning disable CS0618
[Test] [Test]
public void TestLegacyComboIncrease() public void TestLegacyComboIncrease()
@ -330,29 +256,6 @@ namespace osu.Game.Tests.Rulesets.Scoring
Assert.That(HitResult.LegacyComboIncrease.IsHit(), Is.True); Assert.That(HitResult.LegacyComboIncrease.IsHit(), Is.True);
Assert.That(HitResult.LegacyComboIncrease.IsScorable(), Is.True); Assert.That(HitResult.LegacyComboIncrease.IsScorable(), Is.True);
Assert.That(HitResultExtensions.ALL_TYPES, Does.Not.Contain(HitResult.LegacyComboIncrease)); 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 #pragma warning restore CS0618
@ -362,36 +265,30 @@ namespace osu.Game.Tests.Rulesets.Scoring
const int count_judgements = 1000; const int count_judgements = 1000;
const int count_misses = 1; 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++)
{ {
{ HitResult.Great, count_judgements - count_misses }, scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[i], new TestJudgement(HitResult.Great))
{ HitResult.Miss, count_misses } {
} Type = i == 0 ? HitResult.Miss : HitResult.Great
}); });
}
const double expected = (count_judgements - count_misses) / (double)count_judgements; 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(0.0));
Assert.That(actual, Is.Not.EqualTo(1.0)); Assert.That(actual, Is.Not.EqualTo(1.0));
Assert.That(actual, Is.EqualTo(expected).Within(Precision.FLOAT_EPSILON)); 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 private class TestJudgement : Judgement
{ {
public override HitResult MaxResult { get; } public override HitResult MaxResult { get; }
@ -419,14 +316,18 @@ namespace osu.Game.Tests.Rulesets.Scoring
private partial class TestScoreProcessor : ScoreProcessor private partial class TestScoreProcessor : ScoreProcessor
{ {
protected override double DefaultAccuracyPortion => 0.5;
protected override double DefaultComboPortion => 0.5;
public TestScoreProcessor() public TestScoreProcessor()
: base(new TestRuleset()) : 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 // ReSharper disable once MemberHidesStaticFromOuterClass
private class TestRuleset : Ruleset private class TestRuleset : Ruleset
{ {

View File

@ -73,7 +73,5 @@ namespace osu.Game.Tests.Rulesets
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null; public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null;
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null; public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null;
} }
#nullable enable
} }
} }

View File

@ -286,7 +286,7 @@ namespace osu.Game.Tests.Visual.Background
this.renderer = renderer; this.renderer = renderer;
} }
protected override Texture GetBackground() => renderer.CreateTexture(1, 1); public override Texture GetBackground() => renderer.CreateTexture(1, 1);
} }
private partial class TestWorkingBeatmapWithStoryboard : TestWorkingBeatmap private partial class TestWorkingBeatmapWithStoryboard : TestWorkingBeatmap

View File

@ -264,8 +264,9 @@ namespace osu.Game.Tests.Visual.Collections
assertCollectionName(1, "First"); assertCollectionName(1, "First");
} }
[Test] [TestCase(false)]
public void TestCollectionRenamedOnTextChange() [TestCase(true)]
public void TestCollectionRenamedOnTextChange(bool commitWithEnter)
{ {
BeatmapCollection first = null!; BeatmapCollection first = null!;
DrawableCollectionListItem firstItem = null!; DrawableCollectionListItem firstItem = null!;
@ -293,9 +294,19 @@ namespace osu.Game.Tests.Visual.Collections
AddStep("change first collection name", () => AddStep("change first collection name", () =>
{ {
firstItem.ChildrenOfType<TextBox>().First().Text = "First"; firstItem.ChildrenOfType<TextBox>().First().Text = "First";
InputManager.Key(Key.Enter);
}); });
if (commitWithEnter)
AddStep("commit via enter", () => InputManager.Key(Key.Enter));
else
{
AddStep("commit via click away", () =>
{
InputManager.MoveMouseTo(firstItem.ScreenSpaceDrawQuad.TopLeft - new Vector2(10));
InputManager.Click(MouseButton.Left);
});
}
AddUntilStep("collection has new name", () => first.Name == "First"); AddUntilStep("collection has new name", () => first.Name == "First");
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@ -23,8 +21,8 @@ namespace osu.Game.Tests.Visual.Editing
{ {
public partial class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene public partial class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene
{ {
private BeatDivisorControl beatDivisorControl; private BeatDivisorControl beatDivisorControl = null!;
private BindableBeatDivisor bindableBeatDivisor; private BindableBeatDivisor bindableBeatDivisor = null!;
private SliderBar<int> tickSliderBar => beatDivisorControl.ChildrenOfType<SliderBar<int>>().Single(); private SliderBar<int> tickSliderBar => beatDivisorControl.ChildrenOfType<SliderBar<int>>().Single();
private Triangle tickMarkerHead => tickSliderBar.ChildrenOfType<Triangle>().Single(); private Triangle tickMarkerHead => tickSliderBar.ChildrenOfType<Triangle>().Single();
@ -42,7 +40,8 @@ namespace osu.Game.Tests.Visual.Editing
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(90, 90) Size = new Vector2(90, 90),
Scale = new Vector2(3),
} }
}; };
}); });
@ -50,9 +49,9 @@ namespace osu.Game.Tests.Visual.Editing
[Test] [Test]
public void TestBindableBeatDivisor() public void TestBindableBeatDivisor()
{ {
AddRepeatStep("move previous", () => bindableBeatDivisor.Previous(), 2); AddRepeatStep("move previous", () => bindableBeatDivisor.SelectPrevious(), 2);
AddAssert("divisor is 4", () => bindableBeatDivisor.Value == 4); AddAssert("divisor is 4", () => bindableBeatDivisor.Value == 4);
AddRepeatStep("move next", () => bindableBeatDivisor.Next(), 1); AddRepeatStep("move next", () => bindableBeatDivisor.SelectNext(), 1);
AddAssert("divisor is 12", () => bindableBeatDivisor.Value == 8); AddAssert("divisor is 12", () => bindableBeatDivisor.Value == 8);
} }
@ -64,17 +63,24 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.MoveMouseTo(tickMarkerHead.ScreenSpaceDrawQuad.Centre); InputManager.MoveMouseTo(tickMarkerHead.ScreenSpaceDrawQuad.Centre);
InputManager.PressButton(MouseButton.Left); 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); 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("hold marker", () => InputManager.PressButton(MouseButton.Left));
AddStep("move to 16", () => InputManager.MoveMouseTo(getPositionForDivisor(16))); AddStep("move to ~6 and release", () =>
AddStep("move to ~10 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.MoveMouseTo(getPositionForDivisor(10));
InputManager.PressButton(MouseButton.Left);
InputManager.ReleaseButton(MouseButton.Left); InputManager.ReleaseButton(MouseButton.Left);
}); });
AddAssert("divisor clamped to 8", () => bindableBeatDivisor.Value == 8); AddAssert("divisor clamped to 8", () => bindableBeatDivisor.Value == 8);
@ -82,28 +88,33 @@ namespace osu.Game.Tests.Visual.Editing
private Vector2 getPositionForDivisor(int divisor) private Vector2 getPositionForDivisor(int divisor)
{ {
float relativePosition = (float)Math.Clamp(divisor, 0, 16) / 16; float localX = (1 - 1 / (float)divisor) * tickSliderBar.UsableWidth + tickSliderBar.RangePadding;
var sliderDrawQuad = tickSliderBar.ScreenSpaceDrawQuad; return tickSliderBar.ToScreenSpace(new Vector2(
return new Vector2( localX,
sliderDrawQuad.TopLeft.X + sliderDrawQuad.Width * relativePosition, tickSliderBar.DrawHeight / 2
sliderDrawQuad.Centre.Y ));
);
} }
[Test] [Test]
public void TestBeatChevronNavigation() public void TestBeatChevronNavigation()
{ {
switchBeatSnap(1); switchBeatSnap(1);
assertBeatSnap(16);
switchBeatSnap(-4);
assertBeatSnap(1); assertBeatSnap(1);
switchBeatSnap(3); switchBeatSnap(3);
assertBeatSnap(8); assertBeatSnap(8);
switchBeatSnap(-1); switchBeatSnap(3);
assertBeatSnap(16);
switchBeatSnap(-2);
assertBeatSnap(4); assertBeatSnap(4);
switchBeatSnap(-3); switchBeatSnap(-3);
assertBeatSnap(16); assertBeatSnap(1);
} }
[Test] [Test]
@ -156,9 +167,11 @@ namespace osu.Game.Tests.Visual.Editing
switchPresets(1); switchPresets(1);
assertPreset(BeatDivisorType.Triplets); assertPreset(BeatDivisorType.Triplets);
assertBeatSnap(6);
switchPresets(1); switchPresets(1);
assertPreset(BeatDivisorType.Common); assertPreset(BeatDivisorType.Common);
assertBeatSnap(4);
switchPresets(-1); switchPresets(-1);
assertPreset(BeatDivisorType.Triplets); assertPreset(BeatDivisorType.Triplets);
@ -174,6 +187,7 @@ namespace osu.Game.Tests.Visual.Editing
setDivisorViaInput(15); setDivisorViaInput(15);
assertPreset(BeatDivisorType.Custom, 15); assertPreset(BeatDivisorType.Custom, 15);
assertBeatSnap(15);
switchBeatSnap(-1); switchBeatSnap(-1);
assertBeatSnap(5); assertBeatSnap(5);
@ -183,12 +197,14 @@ namespace osu.Game.Tests.Visual.Editing
setDivisorViaInput(5); setDivisorViaInput(5);
assertPreset(BeatDivisorType.Custom, 15); assertPreset(BeatDivisorType.Custom, 15);
assertBeatSnap(5);
switchPresets(1); switchPresets(1);
assertPreset(BeatDivisorType.Common); assertPreset(BeatDivisorType.Common);
switchPresets(-1); switchPresets(-1);
assertPreset(BeatDivisorType.Triplets); assertPreset(BeatDivisorType.Custom, 15);
assertBeatSnap(15);
} }
private void switchBeatSnap(int direction) => AddRepeatStep($"move snap {(direction > 0 ? "forward" : "backward")}", () => private void switchBeatSnap(int direction) => AddRepeatStep($"move snap {(direction > 0 ? "forward" : "backward")}", () =>
@ -200,7 +216,7 @@ namespace osu.Game.Tests.Visual.Editing
}, Math.Abs(direction)); }, Math.Abs(direction));
private void assertBeatSnap(int expected) => AddAssert($"beat snap is {expected}", private void assertBeatSnap(int expected) => AddAssert($"beat snap is {expected}",
() => bindableBeatDivisor.Value == expected); () => bindableBeatDivisor.Value, () => Is.EqualTo(expected));
private void switchPresets(int direction) => AddRepeatStep($"move presets {(direction > 0 ? "forward" : "backward")}", () => private void switchPresets(int direction) => AddRepeatStep($"move presets {(direction > 0 ? "forward" : "backward")}", () =>
{ {
@ -212,7 +228,7 @@ namespace osu.Game.Tests.Visual.Editing
private void assertPreset(BeatDivisorType type, int? maxDivisor = null) private void assertPreset(BeatDivisorType type, int? maxDivisor = null)
{ {
AddAssert($"preset is {type}", () => bindableBeatDivisor.ValidDivisors.Value.Type == type); AddAssert($"preset is {type}", () => bindableBeatDivisor.ValidDivisors.Value.Type, () => Is.EqualTo(type));
if (type == BeatDivisorType.Custom) if (type == BeatDivisorType.Custom)
{ {
@ -230,7 +246,7 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
BeatDivisorControl.CustomDivisorPopover popover = null; BeatDivisorControl.CustomDivisorPopover? popover = null;
AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType<BeatDivisorControl.CustomDivisorPopover>().SingleOrDefault()) != null && popover.IsLoaded); AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType<BeatDivisorControl.CustomDivisorPopover>().SingleOrDefault()) != null && popover.IsLoaded);
AddStep($"set divisor to {divisor}", () => AddStep($"set divisor to {divisor}", () =>
{ {

View File

@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual.Editing
private class SnapProvider : IDistanceSnapProvider 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); public Bindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1);

View File

@ -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));
}
}
}

View File

@ -209,10 +209,14 @@ namespace osu.Game.Tests.Visual.Editing
public override void TearDownSteps() public override void TearDownSteps()
{ {
base.TearDownSteps(); base.TearDownSteps();
AddStep("delete imported", () => AddStep("delete imported", () => Realm.Write(r =>
{ {
beatmaps.Delete(importedBeatmapSet); // delete from realm directly rather than via `BeatmapManager` to avoid cross-test pollution
}); // (`BeatmapManager.Delete()` uses soft deletion, which can lead to beatmap reuse between test cases).
r.RemoveAll<BeatmapMetadata>();
r.RemoveAll<BeatmapInfo>();
r.RemoveAll<BeatmapSetInfo>();
}));
} }
} }
} }

View File

@ -13,6 +13,7 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; 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, Position = (OsuPlayfield.BASE_SIZE + new Vector2(100, 0)) / 2,
Samples = new List<HitSampleInfo> 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"); hitObjectHasSampleBank(0, "normal");
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP); hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
hitObjectHasSampleBank(1, "soft"); hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP); hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
AddStep("remove clap addition", () => InputManager.Key(Key.R)); AddStep("remove clap addition", () => InputManager.Key(Key.R));
hitObjectHasSampleBank(0, "normal"); hitObjectHasSampleBank(0, "normal");
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL); hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL);
hitObjectHasSampleBank(1, "soft"); hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL); hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL);
} }
@ -89,7 +90,7 @@ namespace osu.Game.Tests.Visual.Editing
public void TestSingleSelection() public void TestSingleSelection()
{ {
clickSamplePiece(0); clickSamplePiece(0);
samplePopoverHasSingleBank("normal"); samplePopoverHasSingleBank(HitSampleInfo.BANK_NORMAL);
samplePopoverHasSingleVolume(80); samplePopoverHasSingleVolume(80);
dismissPopover(); dismissPopover();
@ -99,21 +100,21 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.First())); AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.First()));
clickSamplePiece(1); clickSamplePiece(1);
samplePopoverHasSingleBank("soft"); samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
samplePopoverHasSingleVolume(60); samplePopoverHasSingleVolume(60);
setVolumeViaPopover(90); setVolumeViaPopover(90);
hitObjectHasSampleVolume(1, 90); hitObjectHasSampleVolume(1, 90);
setBankViaPopover("drum"); setBankViaPopover(HitSampleInfo.BANK_DRUM);
hitObjectHasSampleBank(1, "drum"); hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
} }
[Test] [Test]
public void TestUndo() public void TestUndo()
{ {
clickSamplePiece(1); clickSamplePiece(1);
samplePopoverHasSingleBank("soft"); samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
samplePopoverHasSingleVolume(60); samplePopoverHasSingleVolume(60);
setVolumeViaPopover(90); setVolumeViaPopover(90);
@ -170,7 +171,7 @@ namespace osu.Game.Tests.Visual.Editing
} }
[Test] [Test]
public void TestMultipleSelectionWithSameSampleBank() public void TestPopoverMultipleSelectionWithSameSampleBank()
{ {
AddStep("unify sample bank", () => AddStep("unify sample bank", () =>
{ {
@ -178,33 +179,33 @@ namespace osu.Game.Tests.Visual.Editing
{ {
for (int i = 0; i < h.Samples.Count; i++) 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)); AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickSamplePiece(0); clickSamplePiece(0);
samplePopoverHasSingleBank("soft"); samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
dismissPopover(); dismissPopover();
clickSamplePiece(1); clickSamplePiece(1);
samplePopoverHasSingleBank("soft"); samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
setBankViaPopover(string.Empty); setBankViaPopover(string.Empty);
hitObjectHasSampleBank(0, "soft"); hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleBank(1, "soft"); hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
samplePopoverHasSingleBank("soft"); samplePopoverHasSingleBank(HitSampleInfo.BANK_SOFT);
setBankViaPopover("drum"); setBankViaPopover(HitSampleInfo.BANK_DRUM);
hitObjectHasSampleBank(0, "drum"); hitObjectHasSampleBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleBank(1, "drum"); hitObjectHasSampleBank(1, HitSampleInfo.BANK_DRUM);
samplePopoverHasSingleBank("drum"); samplePopoverHasSingleBank(HitSampleInfo.BANK_DRUM);
} }
[Test] [Test]
public void TestMultipleSelectionWithDifferentSampleBank() public void TestPopoverMultipleSelectionWithDifferentSampleBank()
{ {
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickSamplePiece(0); clickSamplePiece(0);
@ -216,14 +217,109 @@ namespace osu.Game.Tests.Visual.Editing
samplePopoverHasIndeterminateBank(); samplePopoverHasIndeterminateBank();
setBankViaPopover(string.Empty); setBankViaPopover(string.Empty);
hitObjectHasSampleBank(0, "normal"); hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleBank(1, "soft"); hitObjectHasSampleBank(1, HitSampleInfo.BANK_SOFT);
samplePopoverHasIndeterminateBank(); samplePopoverHasIndeterminateBank();
setBankViaPopover("normal"); setBankViaPopover(HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleBank(0, "normal"); hitObjectHasSampleBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleBank(1, "normal"); hitObjectHasSampleBank(1, HitSampleInfo.BANK_NORMAL);
samplePopoverHasSingleBank("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", () => private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () =>

View File

@ -8,6 +8,8 @@ using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Tests.Visual.Ranking; using osu.Game.Tests.Visual.Ranking;
@ -49,6 +51,21 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any()); AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
} }
[Test]
public void TestModRemovingTimedInputs()
{
AddStep("Set score with mod removing timed inputs", () =>
{
offsetControl.ReferenceScore.Value = new ScoreInfo
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(10),
Mods = new Mod[] { new OsuModRelax() }
};
});
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
}
[Test] [Test]
public void TestCalibrationFromZero() public void TestCalibrationFromZero()
{ {

View File

@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.Gameplay
new HitCircle new HitCircle
{ {
StartTime = t += spacing, StartTime = t += spacing,
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft") }, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT) },
}, },
new HitCircle new HitCircle
{ {
@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
StartTime = t += spacing, StartTime = t += spacing,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }), 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) },
}, },
}); });

View File

@ -31,8 +31,8 @@ namespace osu.Game.Tests.Visual.Gameplay
private HUDOverlay hudOverlay = null!; private HUDOverlay hudOverlay = null!;
[Cached] [Cached(typeof(ScoreProcessor))]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor;
[Cached(typeof(HealthProcessor))] [Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);

View File

@ -22,6 +22,7 @@ using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring.Legacy;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input; using osuTK.Input;
@ -124,8 +125,8 @@ namespace osu.Game.Tests.Visual.Gameplay
graphs.Clear(); graphs.Clear();
legend.Clear(); legend.Clear();
runForProcessor("lazer-standardised", Color4.YellowGreen, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Standardised } }); runForProcessor("lazer-standardised", Color4.YellowGreen, new ScoreProcessor(new OsuRuleset()), ScoringMode.Standardised);
runForProcessor("lazer-classic", Color4.MediumPurple, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Classic } }); runForProcessor("lazer-classic", Color4.MediumPurple, new ScoreProcessor(new OsuRuleset()), ScoringMode.Classic);
runScoreV1(); runScoreV1();
runScoreV2(); 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; 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.Great }),
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }), () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }),
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }), () => 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; int maxCombo = sliderMaxCombo.Current.Value;

View File

@ -22,8 +22,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public partial class TestSceneSkinEditorMultipleSkins : SkinnableTestScene public partial class TestSceneSkinEditorMultipleSkins : SkinnableTestScene
{ {
[Cached] [Cached(typeof(ScoreProcessor))]
private readonly ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor;
[Cached(typeof(HealthProcessor))] [Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);

View File

@ -28,8 +28,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
private HUDOverlay hudOverlay; private HUDOverlay hudOverlay;
[Cached] [Cached(typeof(ScoreProcessor))]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor;
[Cached(typeof(HealthProcessor))] [Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);

View File

@ -10,13 +10,14 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Tests.Gameplay;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
public partial class TestSceneSkinnableScoreCounter : SkinnableHUDComponentTestScene public partial class TestSceneSkinnableScoreCounter : SkinnableHUDComponentTestScene
{ {
[Cached] [Cached(typeof(ScoreProcessor))]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); private ScoreProcessor scoreProcessor = TestGameplayState.Create(new OsuRuleset()).ScoreProcessor;
protected override Drawable CreateDefaultImplementation() => new DefaultScoreCounter(); protected override Drawable CreateDefaultImplementation() => new DefaultScoreCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyScoreCounter(); protected override Drawable CreateLegacyImplementation() => new LegacyScoreCounter();

View File

@ -16,13 +16,14 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Tests.Gameplay;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
public partial class TestSceneSoloGameplayLeaderboard : OsuTestScene public partial class TestSceneSoloGameplayLeaderboard : OsuTestScene
{ {
[Cached] [Cached(typeof(ScoreProcessor))]
private readonly ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); private readonly ScoreProcessor scoreProcessor = TestGameplayState.Create(new OsuRuleset()).ScoreProcessor;
private readonly BindableList<ScoreInfo> scores = new BindableList<ScoreInfo>(); private readonly BindableList<ScoreInfo> scores = new BindableList<ScoreInfo>();

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -8,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.IO; 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")); 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] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -102,7 +116,7 @@ namespace osu.Game.Tests.Visual.Gameplay
decoupledClock.ChangeSource(Beatmap.Value.Track); decoupledClock.ChangeSource(Beatmap.Value.Track);
} }
private void loadStoryboard(string filename) private void loadStoryboard(string filename, Action<Storyboard>? setUpStoryboard = null)
{ {
Storyboard loaded; Storyboard loaded;
@ -113,6 +127,8 @@ namespace osu.Game.Tests.Visual.Gameplay
loaded = decoder.Decode(bfr); loaded = decoder.Decode(bfr);
} }
setUpStoryboard?.Invoke(loaded);
loadStoryboard(loaded); loadStoryboard(loaded);
} }
} }

View 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
});
}
}

View File

@ -21,7 +21,6 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy; using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
@ -188,15 +187,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
if (!lastHeaders.TryGetValue(userId, out var header)) 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.Miss] = 0,
[HitResult.Meh] = 0, [HitResult.Meh] = 0,
[HitResult.Great] = 0 [HitResult.Great] = 0
} }, new ScoreProcessorStatistics(), DateTimeOffset.Now);
});
} }
switch (RNG.Next(0, 3)) switch (RNG.Next(0, 3))

View File

@ -129,7 +129,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
Playlist = Playlist =
{ {
new MultiplayerPlaylistItem(playlistItem), TestMultiplayerClient.CreateMultiplayerPlaylistItem(playlistItem),
}, },
Users = { localUser }, Users = { localUser },
Host = localUser, Host = localUser,

View File

@ -906,7 +906,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
enterGameplay(); enterGameplay();
AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 })); AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 }));
AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem( AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, TestMultiplayerClient.CreateMultiplayerPlaylistItem(
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{ {
RulesetID = new OsuRuleset().RulesetInfo.OnlineID, RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
@ -938,7 +938,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
enterGameplay(); enterGameplay();
AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 })); AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 }));
AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem( AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, TestMultiplayerClient.CreateMultiplayerPlaylistItem(
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{ {
RulesetID = new OsuRuleset().RulesetInfo.OnlineID, RulesetID = new OsuRuleset().RulesetInfo.OnlineID,

View File

@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
/// </summary> /// </summary>
private void addItemStep(bool expired = false, int? userId = null) => AddStep("add item", () => private void addItemStep(bool expired = false, int? userId = null) => AddStep("add item", () =>
{ {
MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap) MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, TestMultiplayerClient.CreateMultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)
{ {
Expired = expired, Expired = expired,
PlayedAt = DateTimeOffset.Now PlayedAt = DateTimeOffset.Now

View File

@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("add playlist item", () => AddStep("add playlist item", () =>
{ {
MultiplayerPlaylistItem item = new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)); MultiplayerPlaylistItem item = TestMultiplayerClient.CreateMultiplayerPlaylistItem(new PlaylistItem(importedBeatmap));
MultiplayerClient.AddUserPlaylistItem(userId(), item).WaitSafely(); MultiplayerClient.AddUserPlaylistItem(userId(), item).WaitSafely();

View File

@ -3,15 +3,61 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps; 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;
using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osu.Game.Tests.Resources;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Navigation namespace osu.Game.Tests.Visual.Navigation
{ {
public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene 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", () => getEditor().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> /// <summary>
/// When entering the editor, a new beatmap is created as part of the asynchronous load process. /// 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) /// This test ensures that in the case of an early exit from the editor (ie. while it's still loading)
@ -38,5 +84,94 @@ namespace osu.Game.Tests.Visual.Navigation
BeatmapSetInfo[] allBeatmapSets() => Game.Realm.Run(realm => realm.All<BeatmapSetInfo>().Where(x => !x.DeletePending).ToArray()); 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);
}
[Test]
public void TestLastTimestampRememberedOnExit()
{
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("seek to arbitrary time", () => getEditor().ChildrenOfType<EditorClock>().First().Seek(1234));
AddUntilStep("time is correct", () => getEditor().ChildrenOfType<EditorClock>().First().CurrentTime, () => Is.EqualTo(1234));
AddStep("exit editor", () => InputManager.Key(Key.Escape));
AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor);
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit());
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
AddUntilStep("time is correct", () => getEditor().ChildrenOfType<EditorClock>().First().CurrentTime, () => Is.EqualTo(1234));
}
private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType<EditorBeatmap>().Single();
private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen;
} }
} }

View File

@ -17,6 +17,7 @@ using osu.Game.Beatmaps;
using osu.Game.Collections; using osu.Game.Collections;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Leaderboards; using osu.Game.Online.Leaderboards;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.BeatmapListing;
@ -539,6 +540,11 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("open room", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().Single().Open()); AddStep("open room", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().Single().Open());
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action()); AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
AddWaitStep("wait two frames", 2); AddWaitStep("wait two frames", 2);
AddStep("exit lounge", () => Game.ScreenStack.Exit());
// `TestMultiplayerComponents` registers a request handler in its BDL, but never unregisters it.
// to prevent the handler living for longer than it should be, clean up manually.
AddStep("clean up multiplayer request handler", () => ((DummyAPIAccess)API).HandleRequest = null);
} }
[Test] [Test]

View File

@ -174,78 +174,33 @@ namespace osu.Game.Tests.Visual.Ranking
private class TestRulesetAllStatsRequireHitEvents : TestRuleset private class TestRulesetAllStatsRequireHitEvents : TestRuleset
{ {
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
{ {
return new[] new StatisticItem("Statistic Requiring Hit Events 1", () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true),
{ new StatisticItem("Statistic Requiring Hit Events 2", () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true)
new StatisticRow
{
Columns = new[]
{
new StatisticItem("Statistic Requiring Hit Events 1",
() => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true)
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem("Statistic Requiring Hit Events 2",
() => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true)
}
}
}; };
} }
}
private class TestRulesetNoStatsRequireHitEvents : TestRuleset private class TestRulesetNoStatsRequireHitEvents : TestRuleset
{ {
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{ {
return new[] return new[]
{ {
new StatisticRow new StatisticItem("Statistic Not Requiring Hit Events 1", () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events")),
{ new StatisticItem("Statistic Not Requiring Hit Events 2", () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events"))
Columns = new[]
{
new StatisticItem("Statistic Not Requiring Hit Events 1",
() => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events"))
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem("Statistic Not Requiring Hit Events 2",
() => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events"))
}
}
}; };
} }
} }
private class TestRulesetMixed : TestRuleset private class TestRulesetMixed : TestRuleset
{ {
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{ {
return new[] return new[]
{ {
new StatisticRow new StatisticItem("Statistic Requiring Hit Events", () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true),
{ new StatisticItem("Statistic Not Requiring Hit Events", () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events"))
Columns = new[]
{
new StatisticItem("Statistic Requiring Hit Events",
() => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true)
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem("Statistic Not Requiring Hit Events",
() => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events"))
}
}
}; };
} }
} }

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -12,7 +11,6 @@ using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Carousel;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
@ -143,25 +141,20 @@ namespace osu.Game.Tests.Visual.SongSelect
testScoreInfo.User = API.LocalUser.Value; testScoreInfo.User = API.LocalUser.Value;
testScoreInfo.Rank = ScoreRank.B; testScoreInfo.Rank = ScoreRank.B;
testScoreInfo.TotalScore = scoreManager.GetTotalScore(testScoreInfo, ScoringMode.Classic);
scoreManager.Import(testScoreInfo); scoreManager.Import(testScoreInfo);
}); });
AddUntilStep("B rank displayed", () => topLocalRank.DisplayedRank == ScoreRank.B); 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); var testScoreInfo2 = TestResources.CreateTestScoreInfo(importedBeatmap);
testScoreInfo2.User = API.LocalUser.Value; testScoreInfo2.User = API.LocalUser.Value;
testScoreInfo2.Rank = ScoreRank.X; testScoreInfo2.Rank = ScoreRank.X;
testScoreInfo2.Statistics = testScoreInfo2.MaximumStatistics; testScoreInfo2.Statistics = testScoreInfo2.MaximumStatistics;
testScoreInfo2.TotalScore = scoreManager.GetTotalScore(testScoreInfo2); testScoreInfo2.TotalScore = testScoreInfo.TotalScore + 1;
// 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);
scoreManager.Import(testScoreInfo2); scoreManager.Import(testScoreInfo2);
}); });

View File

@ -80,6 +80,24 @@ namespace osu.Game.Tests.Visual.UserInterface
}); });
} }
[Test]
public void TestCorrectScrollToWhenContentLoads()
{
AddRepeatStep("add many sections", () => append(1f), 3);
AddStep("add section with delayed load content", () =>
{
container.Add(new TestDelayedLoadSection("delayed"));
});
AddStep("add final section", () => append(0.5f));
AddStep("scroll to final section", () => container.ScrollTo(container.Children.Last()));
AddUntilStep("correct section selected", () => container.SelectedSection.Value == container.Children.Last());
AddUntilStep("wait for scroll to section", () => container.ScreenSpaceDrawQuad.AABBFloat.Contains(container.Children.Last().ScreenSpaceDrawQuad.AABBFloat));
}
[Test] [Test]
public void TestSelection() public void TestSelection()
{ {
@ -196,6 +214,33 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.ScrollVerticalBy(direction); InputManager.ScrollVerticalBy(direction);
} }
private partial class TestDelayedLoadSection : TestSection
{
public TestDelayedLoadSection(string label)
: base(label)
{
BackgroundColour = default_colour;
Width = 300;
AutoSizeAxes = Axes.Y;
}
protected override void LoadComplete()
{
base.LoadComplete();
Box box;
Add(box = new Box
{
Alpha = 0.01f,
RelativeSizeAxes = Axes.X,
});
// Emulate an operation that will be inhibited by IsMaskedAway.
box.ResizeHeightTo(2000, 50);
}
}
private partial class TestSection : TestBox private partial class TestSection : TestBox
{ {
public bool Selected public bool Selected

View File

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
@ -137,8 +138,7 @@ namespace osu.Game.Tests.Visual.UserInterface
if (!objects.Any()) if (!objects.Any())
return; return;
double firstHit = objects.First().StartTime; (double firstHit, double lastHit) = BeatmapExtensions.CalculatePlayableBounds(objects);
double lastHit = objects.Max(o => o.GetEndTime());
if (lastHit == 0) if (lastHit == 0)
lastHit = objects.Last().StartTime; lastHit = objects.Last().StartTime;

View File

@ -51,7 +51,7 @@ namespace osu.Game.Tests
protected override IBeatmap GetBeatmap() => beatmap; protected override IBeatmap GetBeatmap() => beatmap;
protected override Texture GetBackground() => null; public override Texture GetBackground() => null;
protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile)); protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile));

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Tournament.IPC namespace osu.Game.Tournament.IPC
{ {
public enum TourneyState public enum TourneyState

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Screens.Ladder.Components namespace osu.Game.Tournament.Screens.Ladder.Components

View File

@ -20,11 +20,20 @@ namespace osu.Game.Audio
public const string HIT_FINISH = @"hitfinish"; public const string HIT_FINISH = @"hitfinish";
public const string HIT_CLAP = @"hitclap"; 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> /// <summary>
/// All valid sample addition constants. /// All valid sample addition constants.
/// </summary> /// </summary>
public static IEnumerable<string> AllAdditions => new[] { HIT_WHISTLE, HIT_FINISH, HIT_CLAP }; 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> /// <summary>
/// The name of the sample to load. /// The name of the sample to load.
/// </summary> /// </summary>

View File

@ -171,6 +171,11 @@ namespace osu.Game.Beatmaps
public double TimelineZoom { get; set; } = 1.0; public double TimelineZoom { get; set; } = 1.0;
/// <summary>
/// The time in milliseconds when last exiting the editor with this beatmap loaded.
/// </summary>
public double? EditorTimestamp { get; set; }
[Ignored] [Ignored]
public CountdownType Countdown { get; set; } = CountdownType.Normal; public CountdownType Countdown { get; set; } = CountdownType.Normal;

View File

@ -0,0 +1,89 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
namespace osu.Game.Beatmaps
{
// Implementation of this class is based off of `MaxDimensionLimitedTextureLoaderStore`.
// If issues are found it's worth checking to make sure similar issues exist there.
public class BeatmapPanelBackgroundTextureLoaderStore : IResourceStore<TextureUpload>
{
// These numbers are taken from the draw visualiser size requirements for song select panel textures at extreme aspect ratios.
private const int max_height = 130;
private const int max_width = 1280;
private readonly IResourceStore<TextureUpload>? textureStore;
public BeatmapPanelBackgroundTextureLoaderStore(IResourceStore<TextureUpload>? textureStore)
{
this.textureStore = textureStore;
}
public void Dispose()
{
textureStore?.Dispose();
}
public TextureUpload Get(string name)
{
var textureUpload = textureStore?.Get(name);
// NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp.
if (textureUpload == null)
return null!;
return limitTextureUploadSize(textureUpload);
}
public async Task<TextureUpload> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken())
{
// NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp.
if (textureStore == null)
return null!;
var textureUpload = await textureStore.GetAsync(name, cancellationToken).ConfigureAwait(false);
if (textureUpload == null)
return null!;
return await Task.Run(() => limitTextureUploadSize(textureUpload), cancellationToken).ConfigureAwait(false);
}
private TextureUpload limitTextureUploadSize(TextureUpload textureUpload)
{
var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
// The original texture upload will no longer be returned or used.
textureUpload.Dispose();
Size size = image.Size();
int usableWidth = Math.Min(max_width, size.Width);
int usableHeight = Math.Min(max_height, size.Height);
// Crop the centre region of the background for now.
Rectangle cropRectangle = new Rectangle(
(size.Width - usableWidth) / 2,
(size.Height - usableHeight) / 2,
usableWidth,
usableHeight
);
image.Mutate(i => i.Crop(cropRectangle));
return new TextureUpload(image);
}
public Stream? GetStream(string name) => textureStore?.GetStream(name);
public IEnumerable<string> GetAvailableResources() => textureStore?.GetAvailableResources() ?? Array.Empty<string>();
}
}

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Logging; using osu.Framework.Logging;
@ -11,7 +10,6 @@ using osu.Framework.Platform;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
@ -74,7 +72,7 @@ namespace osu.Game.Beatmaps
var calculator = ruleset.CreateDifficultyCalculator(working); var calculator = ruleset.CreateDifficultyCalculator(working);
beatmap.StarRating = calculator.Calculate().StarRating; beatmap.StarRating = calculator.Calculate().StarRating;
beatmap.Length = calculateLength(working.Beatmap); beatmap.Length = working.Beatmap.CalculatePlayableLength();
beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength(); beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength();
} }
@ -82,20 +80,6 @@ namespace osu.Game.Beatmaps
workingBeatmapCache.Invalidate(beatmapSet); 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 #region Implementation of IDisposable
public void Dispose() public void Dispose()

View File

@ -14,7 +14,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </remarks> /// </remarks>
public class SampleControlPoint : ControlPoint, IEquatable<SampleControlPoint> 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 public static readonly SampleControlPoint DEFAULT = new SampleControlPoint
{ {

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
public enum DifficultyRating public enum DifficultyRating

View File

@ -23,8 +23,9 @@ namespace osu.Game.Beatmaps.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
if (working.Background != null) var background = working.GetBackground();
Texture = working.Background; if (background != null)
Texture = background;
} }
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Beatmaps.Drawables.Cards namespace osu.Game.Beatmaps.Drawables.Cards
{ {
/// <summary> /// <summary>

View File

@ -52,7 +52,7 @@ namespace osu.Game.Beatmaps
protected override IBeatmap GetBeatmap() => new Beatmap(); protected override IBeatmap GetBeatmap() => new Beatmap();
protected override Texture GetBackground() => textures?.Get(@"Backgrounds/bg4"); public override Texture GetBackground() => textures?.Get(@"Backgrounds/bg4");
protected override Track GetBeatmapTrack() => GetVirtualTrack(); protected override Track GetBeatmapTrack() => GetVirtualTrack();

View File

@ -43,7 +43,7 @@ namespace osu.Game.Beatmaps
} }
protected override IBeatmap GetBeatmap() => beatmap; protected override IBeatmap GetBeatmap() => beatmap;
protected override Texture GetBackground() => throw new NotImplementedException(); public override Texture GetBackground() => throw new NotImplementedException();
protected override Track GetBeatmapTrack() => throw new NotImplementedException(); protected override Track GetBeatmapTrack() => throw new NotImplementedException();
protected internal override ISkin GetSkin() => throw new NotImplementedException(); protected internal override ISkin GetSkin() => throw new NotImplementedException();
public override Stream GetStream(string storagePath) => throw new NotImplementedException(); public override Stream GetStream(string storagePath) => throw new NotImplementedException();

View File

@ -12,6 +12,7 @@ using System.Linq;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
@ -480,7 +481,7 @@ namespace osu.Game.Beatmaps.Formats
string stringSampleSet = sampleSet.ToString().ToLowerInvariant(); string stringSampleSet = sampleSet.ToString().ToLowerInvariant();
if (stringSampleSet == @"none") if (stringSampleSet == @"none")
stringSampleSet = @"normal"; stringSampleSet = HitSampleInfo.BANK_NORMAL;
if (timingChange) if (timingChange)
{ {

Some files were not shown because too many files have changed in this diff Show More