mirror of
https://github.com/ppy/osu.git
synced 2024-12-13 05:22:54 +08:00
Merge branch 'master' into split-legacy-scoring-attribs
This commit is contained in:
commit
0dcf7a2fa9
39
README.md
39
README.md
@ -12,40 +12,43 @@
|
||||
|
||||
A free-to-win rhythm game. Rhythm is just a *click* away!
|
||||
|
||||
The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge.
|
||||
This is the future – and final – iteration of the [osu!](https://osu.ppy.sh) game client which marks the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge.
|
||||
|
||||
## Status
|
||||
|
||||
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.
|
||||
This project is under constant development, but we do our best to keep things in a stable state. Players are encouraged to install from a release alongside their stable *osu!* client. This project will continue to evolve until we eventually reach the point where most users prefer it over the previous "osu!stable" release.
|
||||
|
||||
**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:
|
||||
A few resources are available as starting points to getting involved and understanding the project:
|
||||
|
||||
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
|
||||
- You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management).
|
||||
- Track our current efforts [towards full "ranked play" support](https://github.com/orgs/ppy/projects/13?query=is%3Aopen+sort%3Aupdated-desc).
|
||||
|
||||
## Running osu!
|
||||
|
||||
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:
|
||||
If you are just looking to give the game a whirl, you can grab the latest release for your platform:
|
||||
|
||||
**Latest release:**
|
||||
### 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) |
|
||||
| ------------- | ------------- | ------------- | ------------- | ------------- |
|
||||
|
||||
- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets.
|
||||
You can also generally download a version for your current device from the [osu! site](https://osu.ppy.sh/home/download).
|
||||
|
||||
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
|
||||
|
||||
**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores in early 2024.
|
||||
|
||||
## Developing a custom ruleset
|
||||
|
||||
osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates).
|
||||
osu! is designed to allow user-created gameplay variations, called "rulesets". Building one of these allows a developer to harness the power of the osu! beatmap library, game engine, and general UX for a new style of gameplay. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates).
|
||||
|
||||
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/discussions/13096).
|
||||
|
||||
## Developing osu!
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Please make sure you have the following prerequisites:
|
||||
|
||||
- A desktop platform with the [.NET 6.0 SDK](https://dotnet.microsoft.com/download) installed.
|
||||
@ -69,9 +72,19 @@ git pull
|
||||
|
||||
### Building
|
||||
|
||||
Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing).
|
||||
#### From an IDE
|
||||
|
||||
- Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln`. This will allow access to template run configurations.
|
||||
You should load the solution via one of the platform-specific `.slnf` files, rather than the main `.sln`. This will reduce dependencies and hide platforms that you don't care about. Valid `.slnf` files are:
|
||||
|
||||
- `osu.Desktop.slnf` (most common)
|
||||
- `osu.Android.slnf`
|
||||
- `osu.iOS.slnf`
|
||||
|
||||
Run configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `osu! (Tests)` project/configuration. More information on this is provided [below](#contributing).
|
||||
|
||||
To build for mobile platforms, you will likely need to run `sudo dotnet workload restore` if you haven't done so previously. This will install Android/iOS tooling required to complete the build.
|
||||
|
||||
#### From CLI
|
||||
|
||||
You can also build and run *osu!* from the command-line with a single command:
|
||||
|
||||
@ -79,12 +92,10 @@ You can also build and run *osu!* from the command-line with a single command:
|
||||
dotnet run --project osu.Desktop
|
||||
```
|
||||
|
||||
If you are not interested in debugging *osu!*, you can add `-c Release` to gain performance. In this case, you must replace `Debug` with `Release` in any commands mentioned in this document.
|
||||
When running locally to do any kind of performance testing, make sure to add `-c Release` to the build command, as the overhead of running with the default `Debug` configuration can be large (especially when testing with local framework modifications as below).
|
||||
|
||||
If the build fails, try to restore NuGet packages with `dotnet restore`.
|
||||
|
||||
_Due to a historical feature gap between .NET Core and Xamarin, running `dotnet` CLI from the root directory will not work for most commands. This can be resolved by specifying a target `.csproj` or the helper project at `build/Desktop.proj`. Configurations have been provided to work around this issue for all supported IDEs mentioned above._
|
||||
|
||||
### 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 quickly achieved using included commands:
|
||||
|
@ -9,9 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
|
||||
|
@ -9,9 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||
|
@ -9,9 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
|
||||
|
@ -9,9 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||
|
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.823.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.922.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -26,7 +26,7 @@
|
||||
<PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
|
||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
<PackageReference Include="System.IO.Packaging" Version="7.0.0" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.1.4.20" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
<EmbeddedResource Include="lazer.ico" />
|
||||
|
@ -7,9 +7,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.4" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.8" />
|
||||
<PackageReference Include="nunit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
|
||||
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
|
||||
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
|
||||
AddAssert("default slider velocity", () => lastObject.SliderVelocityBindable.IsDefault);
|
||||
AddAssert("default slider velocity", () => lastObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
addPlacementSteps(times, positions);
|
||||
addPathCheckStep(times, positions);
|
||||
|
||||
AddAssert("slider velocity changed", () => !lastObject.SliderVelocityBindable.IsDefault);
|
||||
AddAssert("slider velocity changed", () => !lastObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -108,11 +108,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
double[] times = { 100, 300 };
|
||||
float[] positions = { 200, 300 };
|
||||
addBlueprintStep(times, positions);
|
||||
AddAssert("default slider velocity", () => hitObject.SliderVelocityBindable.IsDefault);
|
||||
AddAssert("default slider velocity", () => hitObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||
|
||||
addDragStartStep(times[1], positions[1]);
|
||||
AddMouseMoveStep(times[1], 400);
|
||||
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityBindable.IsDefault);
|
||||
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
157
osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs
Normal file
157
osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs
Normal file
@ -0,0 +1,157 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Judgements;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Scoring;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Tests.Visual.Gameplay;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneScoring : ScoringTestScene
|
||||
{
|
||||
public TestSceneScoring()
|
||||
: base(supportsNonPerfectJudgements: false)
|
||||
{
|
||||
}
|
||||
|
||||
private Bindable<double> scoreMultiplier { get; } = new BindableDouble
|
||||
{
|
||||
Default = 4,
|
||||
Value = 4
|
||||
};
|
||||
|
||||
protected override IBeatmap CreateBeatmap(int maxCombo)
|
||||
{
|
||||
var beatmap = new CatchBeatmap();
|
||||
for (int i = 0; i < maxCombo; ++i)
|
||||
beatmap.HitObjects.Add(new Fruit());
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1 { ScoreMultiplier = { BindTarget = scoreMultiplier } };
|
||||
|
||||
protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo);
|
||||
|
||||
protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new CatchProcessorBasedScoringAlgorithm(beatmap, mode);
|
||||
|
||||
[Test]
|
||||
public void TestBasicScenarios()
|
||||
{
|
||||
AddStep("set up score multiplier", () =>
|
||||
{
|
||||
scoreMultiplier.BindValueChanged(_ => Rerun());
|
||||
});
|
||||
AddStep("set max combo to 100", () => MaxCombo.Value = 100);
|
||||
AddStep("set perfect score", () =>
|
||||
{
|
||||
NonPerfectLocations.Clear();
|
||||
MissLocations.Clear();
|
||||
});
|
||||
AddStep("set score with misses", () =>
|
||||
{
|
||||
NonPerfectLocations.Clear();
|
||||
MissLocations.Clear();
|
||||
MissLocations.AddRange(new[] { 24d, 49 });
|
||||
});
|
||||
AddSliderStep("adjust score multiplier", 0, 10, (int)scoreMultiplier.Default, multiplier => scoreMultiplier.Value = multiplier);
|
||||
}
|
||||
|
||||
private const int base_great = 300;
|
||||
|
||||
private class ScoreV1 : IScoringAlgorithm
|
||||
{
|
||||
private int currentCombo;
|
||||
|
||||
public BindableDouble ScoreMultiplier { get; } = new BindableDouble();
|
||||
|
||||
public void ApplyHit() => applyHitV1(base_great);
|
||||
|
||||
public void ApplyNonPerfect() => throw new NotSupportedException("catch does not have \"non-perfect\" judgements.");
|
||||
|
||||
public void ApplyMiss() => applyHitV1(0);
|
||||
|
||||
private void applyHitV1(int baseScore)
|
||||
{
|
||||
if (baseScore == 0)
|
||||
{
|
||||
currentCombo = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
TotalScore += baseScore;
|
||||
|
||||
// combo multiplier
|
||||
// ReSharper disable once PossibleLossOfFraction
|
||||
TotalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * ScoreMultiplier.Value));
|
||||
|
||||
currentCombo++;
|
||||
}
|
||||
|
||||
public long TotalScore { get; private set; }
|
||||
}
|
||||
|
||||
private class ScoreV2 : IScoringAlgorithm
|
||||
{
|
||||
private int currentCombo;
|
||||
private double comboPortion;
|
||||
|
||||
private readonly double comboPortionMax;
|
||||
|
||||
private const double combo_base = 4;
|
||||
private const int combo_cap = 200;
|
||||
|
||||
public ScoreV2(int maxCombo)
|
||||
{
|
||||
for (int i = 0; i < maxCombo; i++)
|
||||
ApplyHit();
|
||||
|
||||
comboPortionMax = comboPortion;
|
||||
|
||||
currentCombo = 0;
|
||||
comboPortion = 0;
|
||||
}
|
||||
|
||||
public void ApplyHit() => applyHitV2(base_great);
|
||||
|
||||
public void ApplyNonPerfect() => throw new NotSupportedException("catch does not have \"non-perfect\" judgements.");
|
||||
|
||||
private void applyHitV2(int baseScore)
|
||||
{
|
||||
comboPortion += baseScore * Math.Min(Math.Max(0.5, Math.Log(++currentCombo, combo_base)), Math.Log(combo_cap, combo_base));
|
||||
}
|
||||
|
||||
public void ApplyMiss()
|
||||
{
|
||||
currentCombo = 0;
|
||||
}
|
||||
|
||||
public long TotalScore
|
||||
=> (int)Math.Round(1000000 * comboPortion / comboPortionMax); // vast simplification, as we're not doing ticks here.
|
||||
}
|
||||
|
||||
private class CatchProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm
|
||||
{
|
||||
public CatchProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode)
|
||||
: base(beatmap, mode)
|
||||
{
|
||||
}
|
||||
|
||||
protected override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor();
|
||||
|
||||
protected override JudgementResult CreatePerfectJudgementResult() => new CatchJudgementResult(new Fruit(), new CatchJudgement()) { Type = HitResult.Great };
|
||||
|
||||
protected override JudgementResult CreateNonPerfectJudgementResult() => throw new NotSupportedException("catch does not have \"non-perfect\" judgements.");
|
||||
|
||||
protected override JudgementResult CreateMissJudgementResult() => new CatchJudgementResult(new Fruit(), new CatchJudgement()) { Type = HitResult.Miss };
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
ComboOffset = comboData?.ComboOffset ?? 0,
|
||||
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
|
||||
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
|
||||
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1
|
||||
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
|
||||
}.Yield();
|
||||
|
||||
case IHasDuration endTime:
|
||||
|
@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||
{
|
||||
// The SV setting may need to be changed for the current path.
|
||||
var svBindable = hitObject.SliderVelocityBindable;
|
||||
var svBindable = hitObject.SliderVelocityMultiplierBindable;
|
||||
double svToVelocityFactor = hitObject.Velocity / svBindable.Value;
|
||||
double requiredVelocity = path.ComputeRequiredVelocity();
|
||||
|
||||
|
@ -25,6 +25,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
// we're also a ScrollingHitObjectComposer candidate, but can't be everything can we?
|
||||
public partial class CatchHitObjectComposer : DistancedHitObjectComposer<CatchHitObject>
|
||||
{
|
||||
private const float distance_snap_radius = 50;
|
||||
@ -140,7 +141,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
return base.OnPressed(e);
|
||||
}
|
||||
|
||||
protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) =>
|
||||
protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods) =>
|
||||
new DrawableCatchEditorRuleset(ruleset, beatmap, mods)
|
||||
{
|
||||
TimeRangeMultiplier = { BindTarget = timeRangeMultiplier, }
|
||||
|
@ -28,17 +28,17 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
public int RepeatCount { get; set; }
|
||||
|
||||
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
|
||||
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
|
||||
{
|
||||
Precision = 0.01,
|
||||
MinValue = 0.1,
|
||||
MaxValue = 10
|
||||
};
|
||||
|
||||
public double SliderVelocity
|
||||
public double SliderVelocityMultiplier
|
||||
{
|
||||
get => SliderVelocityBindable.Value;
|
||||
set => SliderVelocityBindable.Value = value;
|
||||
get => SliderVelocityMultiplierBindable.Value;
|
||||
set => SliderVelocityMultiplierBindable.Value = value;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
@ -48,10 +48,10 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
private double tickDistanceFactor;
|
||||
|
||||
[JsonIgnore]
|
||||
public double Velocity => velocityFactor * SliderVelocity;
|
||||
public double Velocity => velocityFactor * SliderVelocityMultiplier;
|
||||
|
||||
[JsonIgnore]
|
||||
public double TickDistance => tickDistanceFactor * SliderVelocity;
|
||||
public double TickDistance => tickDistanceFactor * SliderVelocityMultiplier;
|
||||
|
||||
/// <summary>
|
||||
/// The length of one span of this <see cref="JuiceStream"/>.
|
||||
|
@ -21,8 +21,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
public partial class DrawableCatchRuleset : DrawableScrollingRuleset<CatchHitObject>
|
||||
{
|
||||
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Constant;
|
||||
|
||||
protected override bool UserScrollSpeedAdjustment => false;
|
||||
|
||||
public DrawableCatchRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)
|
||||
@ -30,6 +28,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
Direction.Value = ScrollingDirection.Down;
|
||||
TimeRange.Value = GetTimeRange(beatmap.Difficulty.ApproachRate);
|
||||
VisualisationMethod = ScrollVisualisationMethod.Constant;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
PassCondition = () =>
|
||||
{
|
||||
var hitObject = Player.ChildrenOfType<DrawableManiaHitObject>().FirstOrDefault();
|
||||
return hitObject?.Dependencies.Get<IScrollingInfo>().Algorithm is ConstantScrollAlgorithm;
|
||||
return hitObject?.Dependencies.Get<IScrollingInfo>().Algorithm.Value is ConstantScrollAlgorithm;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,63 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneManiaModDoubleTime : ModTestScene
|
||||
{
|
||||
private const double offset = 18;
|
||||
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestHitWindowWithoutDoubleTime() => CreateModTest(new ModTestData
|
||||
{
|
||||
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value != 1,
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
|
||||
Difficulty = { OverallDifficulty = 10 },
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Note { StartTime = 1000 }
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1000 + offset, ManiaAction.Key1)
|
||||
}
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestHitWindowWithDoubleTime() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModDoubleTime(),
|
||||
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value == 1,
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
|
||||
Difficulty = { OverallDifficulty = 10 },
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Note { StartTime = 1000 }
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1000 + offset, ManiaAction.Key1)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
|
||||
IBindable<ScrollingDirection> IScrollingInfo.Direction => Direction;
|
||||
IBindable<double> IScrollingInfo.TimeRange { get; } = new Bindable<double>(5000);
|
||||
IScrollAlgorithm IScrollingInfo.Algorithm { get; } = new ConstantScrollAlgorithm();
|
||||
IBindable<IScrollAlgorithm> IScrollingInfo.Algorithm { get; } = new Bindable<IScrollAlgorithm>(new ConstantScrollAlgorithm());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
175
osu.Game.Rulesets.Mania.Tests/TestSceneScoring.cs
Normal file
175
osu.Game.Rulesets.Mania.Tests/TestSceneScoring.cs
Normal file
@ -0,0 +1,175 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Tests.Visual.Gameplay;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneScoring : ScoringTestScene
|
||||
{
|
||||
protected override IBeatmap CreateBeatmap(int maxCombo)
|
||||
{
|
||||
var beatmap = new ManiaBeatmap(new StageDefinition(5));
|
||||
for (int i = 0; i < maxCombo; ++i)
|
||||
beatmap.HitObjects.Add(new Note());
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1(MaxCombo.Value);
|
||||
protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo);
|
||||
protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new ManiaProcessorBasedScoringAlgorithm(beatmap, mode);
|
||||
|
||||
[Test]
|
||||
public void TestBasicScenarios()
|
||||
{
|
||||
AddStep("set max combo to 100", () => MaxCombo.Value = 100);
|
||||
AddStep("set perfect score", () =>
|
||||
{
|
||||
NonPerfectLocations.Clear();
|
||||
MissLocations.Clear();
|
||||
});
|
||||
AddStep("set score with misses", () =>
|
||||
{
|
||||
NonPerfectLocations.Clear();
|
||||
MissLocations.Clear();
|
||||
MissLocations.AddRange(new[] { 24d, 49 });
|
||||
});
|
||||
AddStep("set score with misses and OKs", () =>
|
||||
{
|
||||
NonPerfectLocations.Clear();
|
||||
MissLocations.Clear();
|
||||
|
||||
NonPerfectLocations.AddRange(new[] { 9d, 19, 29, 39, 59, 69, 79, 89, 99 });
|
||||
MissLocations.AddRange(new[] { 24d, 49 });
|
||||
});
|
||||
}
|
||||
|
||||
private class ScoreV1 : IScoringAlgorithm
|
||||
{
|
||||
private int currentCombo;
|
||||
private double comboAddition = 100;
|
||||
private double totalScoreDouble;
|
||||
private readonly double scoreMultiplier;
|
||||
|
||||
public ScoreV1(int maxCombo)
|
||||
{
|
||||
scoreMultiplier = 500000d / maxCombo;
|
||||
}
|
||||
|
||||
public void ApplyHit() => applyHitV1(320, add => add + 2, 32);
|
||||
public void ApplyNonPerfect() => applyHitV1(100, add => add - 24, 8);
|
||||
public void ApplyMiss() => applyHitV1(0, _ => -56, 0);
|
||||
|
||||
private void applyHitV1(int scoreIncrease, Func<double, double> comboAdditionFunc, int delta)
|
||||
{
|
||||
comboAddition = comboAdditionFunc(comboAddition);
|
||||
if (currentCombo != 0 && currentCombo % 384 == 0)
|
||||
comboAddition = 100;
|
||||
comboAddition = Math.Max(0, Math.Min(comboAddition, 100));
|
||||
double scoreIncreaseD = Math.Sqrt(comboAddition) * delta * scoreMultiplier / 320;
|
||||
|
||||
TotalScore = (long)totalScoreDouble;
|
||||
|
||||
scoreIncreaseD += scoreIncrease * scoreMultiplier / 320;
|
||||
scoreIncrease = (int)scoreIncreaseD;
|
||||
|
||||
TotalScore += scoreIncrease;
|
||||
totalScoreDouble += scoreIncreaseD;
|
||||
|
||||
if (scoreIncrease > 0)
|
||||
currentCombo++;
|
||||
}
|
||||
|
||||
public long TotalScore { get; private set; }
|
||||
}
|
||||
|
||||
private class ScoreV2 : IScoringAlgorithm
|
||||
{
|
||||
private int currentCombo;
|
||||
private double comboPortion;
|
||||
private double currentBaseScore;
|
||||
private double maxBaseScore;
|
||||
private int currentHits;
|
||||
|
||||
private readonly double comboPortionMax;
|
||||
private readonly int maxCombo;
|
||||
|
||||
private const double combo_base = 4;
|
||||
|
||||
public ScoreV2(int maxCombo)
|
||||
{
|
||||
this.maxCombo = maxCombo;
|
||||
|
||||
for (int i = 0; i < this.maxCombo; i++)
|
||||
ApplyHit();
|
||||
|
||||
comboPortionMax = comboPortion;
|
||||
|
||||
currentCombo = 0;
|
||||
comboPortion = 0;
|
||||
currentBaseScore = 0;
|
||||
maxBaseScore = 0;
|
||||
currentHits = 0;
|
||||
}
|
||||
|
||||
public void ApplyHit() => applyHitV2(305, 300);
|
||||
public void ApplyNonPerfect() => applyHitV2(100, 100);
|
||||
|
||||
private void applyHitV2(int hitValue, int baseHitValue)
|
||||
{
|
||||
maxBaseScore += 305;
|
||||
currentBaseScore += hitValue;
|
||||
comboPortion += baseHitValue * Math.Min(Math.Max(0.5, Math.Log(++currentCombo, combo_base)), Math.Log(400, combo_base));
|
||||
|
||||
currentHits++;
|
||||
}
|
||||
|
||||
public void ApplyMiss()
|
||||
{
|
||||
currentHits++;
|
||||
maxBaseScore += 305;
|
||||
currentCombo = 0;
|
||||
}
|
||||
|
||||
public long TotalScore
|
||||
{
|
||||
get
|
||||
{
|
||||
float accuracy = (float)(currentBaseScore / maxBaseScore);
|
||||
|
||||
return (int)Math.Round
|
||||
(
|
||||
200000 * comboPortion / comboPortionMax +
|
||||
800000 * Math.Pow(accuracy, 2 + 2 * accuracy) * ((double)currentHits / maxCombo)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ManiaProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm
|
||||
{
|
||||
public ManiaProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode)
|
||||
: base(beatmap, mode)
|
||||
{
|
||||
}
|
||||
|
||||
protected override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
|
||||
|
||||
protected override JudgementResult CreatePerfectJudgementResult() => new JudgementResult(new Note(), new ManiaJudgement()) { Type = HitResult.Perfect };
|
||||
|
||||
protected override JudgementResult CreateNonPerfectJudgementResult() => new JudgementResult(new Note(), new ManiaJudgement()) { Type = HitResult.Ok };
|
||||
|
||||
protected override JudgementResult CreateMissJudgementResult() => new JudgementResult(new Note(), new ManiaJudgement()) { Type = HitResult.Miss };
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
@ -10,10 +10,11 @@ using System.Linq;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
@ -50,10 +51,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
|
||||
|
||||
double beatLength;
|
||||
if (hitObject.LegacyBpmMultiplier.HasValue)
|
||||
beatLength = timingPoint.BeatLength * hitObject.LegacyBpmMultiplier.Value;
|
||||
else if (hitObject is IHasSliderVelocity hasSliderVelocity)
|
||||
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
|
||||
|
||||
if (hitObject is IHasSliderVelocity hasSliderVelocity)
|
||||
beatLength = LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(hasSliderVelocity, timingPoint, ManiaRuleset.SHORT_NAME);
|
||||
else
|
||||
beatLength = timingPoint.BeatLength;
|
||||
|
||||
|
@ -2,18 +2,22 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osuTK;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
public partial class DrawableManiaEditorRuleset : DrawableManiaRuleset
|
||||
public partial class DrawableManiaEditorRuleset : DrawableManiaRuleset, ISupportConstantAlgorithmToggle
|
||||
{
|
||||
public BindableBool ShowSpeedChanges { get; set; } = new BindableBool();
|
||||
|
||||
public new IScrollingInfo ScrollingInfo => base.ScrollingInfo;
|
||||
|
||||
public DrawableManiaEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods)
|
||||
@ -21,6 +25,13 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Sequential : ScrollVisualisationMethod.Constant, true);
|
||||
}
|
||||
|
||||
protected override Playfield CreatePlayfield() => new ManiaEditorPlayfield(Beatmap.Stages)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
|
@ -21,7 +21,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
public partial class ManiaHitObjectComposer : HitObjectComposer<ManiaHitObject>
|
||||
public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer<ManiaHitObject>
|
||||
{
|
||||
private DrawableManiaEditorRuleset drawableRuleset;
|
||||
private ManiaBeatSnapGrid beatSnapGrid;
|
||||
@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
|
||||
Playfield.GetColumnByPosition(screenSpacePosition);
|
||||
|
||||
protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
{
|
||||
drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods);
|
||||
|
||||
|
47
osu.Game.Rulesets.Mania/Mods/IManiaRateAdjustmentMod.cs
Normal file
47
osu.Game.Rulesets.Mania/Mods/IManiaRateAdjustmentMod.cs
Normal file
@ -0,0 +1,47 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
/// <summary>
|
||||
/// May be attached to rate-adjustment mods to adjust hit windows adjust relative to gameplay rate.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Historically, in osu!mania, hit windows are expected to adjust relative to the gameplay rate such that the real-world hit window remains the same.
|
||||
/// </remarks>
|
||||
public interface IManiaRateAdjustmentMod : IApplicableToDifficulty, IApplicableToHitObject
|
||||
{
|
||||
BindableNumber<double> SpeedChange { get; }
|
||||
|
||||
HitWindows HitWindows { get; set; }
|
||||
|
||||
void IApplicableToDifficulty.ApplyToDifficulty(BeatmapDifficulty difficulty)
|
||||
{
|
||||
HitWindows = new ManiaHitWindows(SpeedChange.Value);
|
||||
HitWindows.SetDifficulty(difficulty.OverallDifficulty);
|
||||
}
|
||||
|
||||
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case Note:
|
||||
hitObject.HitWindows = HitWindows;
|
||||
break;
|
||||
|
||||
case HoldNote hold:
|
||||
hold.Head.HitWindows = HitWindows;
|
||||
hold.Tail.HitWindows = HitWindows;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<ManiaHitObject> drawableRuleset)
|
||||
{
|
||||
var maniaRuleset = (DrawableManiaRuleset)drawableRuleset;
|
||||
maniaRuleset.ScrollMethod = ScrollVisualisationMethod.Constant;
|
||||
maniaRuleset.VisualisationMethod = ScrollVisualisationMethod.Constant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModDaycore : ModDaycore
|
||||
public class ManiaModDaycore : ModDaycore, IManiaRateAdjustmentMod
|
||||
{
|
||||
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModDoubleTime : ModDoubleTime
|
||||
public class ManiaModDoubleTime : ModDoubleTime, IManiaRateAdjustmentMod
|
||||
{
|
||||
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModHalfTime : ModHalfTime
|
||||
public class ManiaModHalfTime : ModHalfTime, IManiaRateAdjustmentMod
|
||||
{
|
||||
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,14 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModNightcore : ModNightcore<ManiaHitObject>
|
||||
public class ManiaModNightcore : ModNightcore<ManiaHitObject>, IManiaRateAdjustmentMod
|
||||
{
|
||||
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,25 @@
|
||||
// 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 osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Scoring
|
||||
{
|
||||
public class ManiaHitWindows : HitWindows
|
||||
{
|
||||
private readonly double multiplier;
|
||||
|
||||
public ManiaHitWindows()
|
||||
: this(1)
|
||||
{
|
||||
}
|
||||
|
||||
public ManiaHitWindows(double multiplier)
|
||||
{
|
||||
this.multiplier = multiplier;
|
||||
}
|
||||
|
||||
public override bool IsHitResultAllowed(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
@ -22,5 +35,12 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override DifficultyRange[] GetRanges() => base.GetRanges().Select(r =>
|
||||
new DifficultyRange(
|
||||
r.Result,
|
||||
r.Min * multiplier,
|
||||
r.Average * multiplier,
|
||||
r.Max * multiplier)).ToArray();
|
||||
}
|
||||
}
|
||||
|
@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
|
||||
Lookup = lookup;
|
||||
ColumnIndex = columnIndex;
|
||||
}
|
||||
|
||||
public override string ToString() => $"[{nameof(ManiaSkinConfigurationLookup)} lookup:{Lookup} col:{ColumnIndex}]";
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
@ -14,7 +13,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Input.Handlers;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
@ -52,22 +50,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
|
||||
|
||||
public ScrollVisualisationMethod ScrollMethod
|
||||
{
|
||||
get => scrollMethod;
|
||||
set
|
||||
{
|
||||
if (IsLoaded)
|
||||
throw new InvalidOperationException($"Can't alter {nameof(ScrollMethod)} after ruleset is already loaded");
|
||||
|
||||
scrollMethod = value;
|
||||
}
|
||||
}
|
||||
|
||||
private ScrollVisualisationMethod scrollMethod = ScrollVisualisationMethod.Sequential;
|
||||
|
||||
protected override ScrollVisualisationMethod VisualisationMethod => scrollMethod;
|
||||
|
||||
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
|
||||
private readonly BindableInt configScrollSpeed = new BindableInt();
|
||||
private double smoothTimeRange;
|
||||
|
@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
new PathControlPoint(new Vector2(0, 6.25f))
|
||||
}),
|
||||
RepeatCount = 1,
|
||||
SliderVelocity = 10
|
||||
SliderVelocityMultiplier = 10
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[TestCase("basic")]
|
||||
[TestCase("colinear-perfect-curve")]
|
||||
[TestCase("slider-ticks")]
|
||||
[TestCase("slider-ticks-edge-case")]
|
||||
[TestCase("slider-paths-edge-case")]
|
||||
[TestCase("repeat-slider")]
|
||||
[TestCase("uneven-repeat-slider")]
|
||||
[TestCase("old-stacking")]
|
||||
|
@ -142,6 +142,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
drawableHitCircle.Scale = new Vector2(2f);
|
||||
|
||||
LoadComponent(drawableHitCircle);
|
||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
||||
mod.ApplyToDrawableHitObject(drawableHitCircle);
|
||||
|
||||
|
@ -505,6 +505,195 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
addClickActionAssert(0, ClickAction.Shake);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInputDoesNotFallThroughOverlappingSliders()
|
||||
{
|
||||
const double time_first_slider = 1000;
|
||||
const double time_second_slider = 1250;
|
||||
Vector2 positionFirstSlider = new Vector2(100, 50);
|
||||
Vector2 positionSecondSlider = new Vector2(100, 80);
|
||||
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
|
||||
|
||||
var hitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = time_first_slider,
|
||||
Position = positionFirstSlider,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(25, 0),
|
||||
})
|
||||
},
|
||||
new Slider
|
||||
{
|
||||
StartTime = time_second_slider,
|
||||
Position = positionSecondSlider,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(25, 0),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
performTest(hitObjects, new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
|
||||
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint, Actions = { OsuAction.LeftButton } },
|
||||
new OsuReplayFrame { Time = time_first_slider + 50, Position = midpoint },
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Ok);
|
||||
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
||||
// the slider head of the first slider prevents the second slider's head from being hit, so the judgement offset should be very late.
|
||||
// this is not strictly done by the hit policy implementation itself (see `OsuModClassic.blockInputToObjectsUnderSliderHead()`),
|
||||
// but we're testing this here anyways to just keep everything related to input handling and note lock in one place.
|
||||
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, referenceHitWindows.WindowFor(HitResult.Meh));
|
||||
addClickActionAssert(0, ClickAction.Hit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOverlappingSlidersDontBlockEachOtherWhenFullyJudged()
|
||||
{
|
||||
const double time_first_slider = 1000;
|
||||
const double time_second_slider = 1600;
|
||||
Vector2 positionFirstSlider = new Vector2(100, 50);
|
||||
Vector2 positionSecondSlider = new Vector2(100, 80);
|
||||
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
|
||||
|
||||
var hitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = time_first_slider,
|
||||
Position = positionFirstSlider,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(25, 0),
|
||||
})
|
||||
},
|
||||
new Slider
|
||||
{
|
||||
StartTime = time_second_slider,
|
||||
Position = positionSecondSlider,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(25, 0),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
performTest(hitObjects, new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
|
||||
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint },
|
||||
// this frame doesn't do anything on lazer, but is REQUIRED for correct playback on stable,
|
||||
// because stable during replay playback only updates game state _when it encounters a replay frame_
|
||||
new OsuReplayFrame { Time = 1250, Position = midpoint },
|
||||
new OsuReplayFrame { Time = time_second_slider + 50, Position = midpoint, Actions = { OsuAction.LeftButton } },
|
||||
new OsuReplayFrame { Time = time_second_slider + 75, Position = midpoint },
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Ok);
|
||||
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
|
||||
addJudgementAssert(hitObjects[1], HitResult.Ok);
|
||||
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, 50);
|
||||
addClickActionAssert(0, ClickAction.Hit);
|
||||
addClickActionAssert(1, ClickAction.Hit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOverlappingHitCirclesDontBlockEachOtherWhenBothVisible()
|
||||
{
|
||||
const double time_first_circle = 1000;
|
||||
const double time_second_circle = 1200;
|
||||
Vector2 positionFirstCircle = new Vector2(100);
|
||||
Vector2 positionSecondCircle = new Vector2(120);
|
||||
var midpoint = (positionFirstCircle + positionSecondCircle) / 2;
|
||||
|
||||
var hitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = time_first_circle,
|
||||
Position = positionFirstCircle,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = time_second_circle,
|
||||
Position = positionSecondCircle,
|
||||
},
|
||||
};
|
||||
|
||||
performTest(hitObjects, new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Time = time_first_circle, Position = midpoint, Actions = { OsuAction.LeftButton } },
|
||||
new OsuReplayFrame { Time = time_first_circle + 25, Position = midpoint },
|
||||
new OsuReplayFrame { Time = time_first_circle + 50, Position = midpoint, Actions = { OsuAction.RightButton } },
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||
addJudgementOffsetAssert(hitObjects[0], 0);
|
||||
|
||||
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
||||
addJudgementOffsetAssert(hitObjects[1], -150);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOverlappingHitCirclesDontBlockEachOtherWhenFullyFadedOut()
|
||||
{
|
||||
const double time_first_circle = 1000;
|
||||
const double time_second_circle = 1200;
|
||||
const double time_third_circle = 1400;
|
||||
Vector2 positionFirstCircle = new Vector2(100);
|
||||
Vector2 positionSecondCircle = new Vector2(200);
|
||||
|
||||
var hitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = time_first_circle,
|
||||
Position = positionFirstCircle,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = time_second_circle,
|
||||
Position = positionSecondCircle,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = time_third_circle,
|
||||
Position = positionFirstCircle,
|
||||
},
|
||||
};
|
||||
|
||||
performTest(hitObjects, new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Time = time_first_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
|
||||
new OsuReplayFrame { Time = time_first_circle + 50, Position = positionFirstCircle },
|
||||
new OsuReplayFrame { Time = time_second_circle - 50, Position = positionSecondCircle },
|
||||
new OsuReplayFrame { Time = time_second_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } },
|
||||
new OsuReplayFrame { Time = time_second_circle + 50, Position = positionSecondCircle },
|
||||
new OsuReplayFrame { Time = time_third_circle - 50, Position = positionFirstCircle },
|
||||
new OsuReplayFrame { Time = time_third_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
|
||||
new OsuReplayFrame { Time = time_third_circle + 50, Position = positionFirstCircle },
|
||||
});
|
||||
|
||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||
addJudgementOffsetAssert(hitObjects[0], 0);
|
||||
|
||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||
addJudgementOffsetAssert(hitObjects[1], 0);
|
||||
|
||||
addJudgementAssert(hitObjects[2], HitResult.Great);
|
||||
addJudgementOffsetAssert(hitObjects[2], 0);
|
||||
}
|
||||
|
||||
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
||||
{
|
||||
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
|
||||
@ -523,6 +712,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
() => judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, () => Is.EqualTo(offset).Within(50));
|
||||
}
|
||||
|
||||
private void addJudgementOffsetAssert(string name, Func<OsuHitObject?> hitObject, double offset)
|
||||
{
|
||||
AddAssert($"{name} @ judged at {offset}",
|
||||
() => judgementResults.Single(r => r.HitObject == hitObject()).TimeOffset, () => Is.EqualTo(offset).Within(50));
|
||||
}
|
||||
|
||||
private void addClickActionAssert(int inputIndex, ClickAction action)
|
||||
=> AddAssert($"input #{inputIndex} resulted in {action}", () => testPolicy.ClickActions[inputIndex], () => Is.EqualTo(action));
|
||||
|
||||
|
176
osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs
Normal file
176
osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs
Normal file
@ -0,0 +1,176 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Tests.Visual.Gameplay;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneScoring : ScoringTestScene
|
||||
{
|
||||
private Bindable<double> scoreMultiplier { get; } = new BindableDouble
|
||||
{
|
||||
Default = 4,
|
||||
Value = 4
|
||||
};
|
||||
|
||||
protected override IBeatmap CreateBeatmap(int maxCombo)
|
||||
{
|
||||
var beatmap = new OsuBeatmap();
|
||||
for (int i = 0; i < maxCombo; i++)
|
||||
beatmap.HitObjects.Add(new HitCircle());
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1 { ScoreMultiplier = { BindTarget = scoreMultiplier } };
|
||||
protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo);
|
||||
protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new OsuProcessorBasedScoringAlgorithm(beatmap, mode);
|
||||
|
||||
[Test]
|
||||
public void TestBasicScenarios()
|
||||
{
|
||||
AddStep("set up score multiplier", () =>
|
||||
{
|
||||
scoreMultiplier.BindValueChanged(_ => Rerun());
|
||||
});
|
||||
AddStep("set max combo to 100", () => MaxCombo.Value = 100);
|
||||
AddStep("set perfect score", () =>
|
||||
{
|
||||
NonPerfectLocations.Clear();
|
||||
MissLocations.Clear();
|
||||
});
|
||||
AddStep("set score with misses", () =>
|
||||
{
|
||||
NonPerfectLocations.Clear();
|
||||
MissLocations.Clear();
|
||||
MissLocations.AddRange(new[] { 24d, 49 });
|
||||
});
|
||||
AddStep("set score with misses and OKs", () =>
|
||||
{
|
||||
NonPerfectLocations.Clear();
|
||||
MissLocations.Clear();
|
||||
|
||||
NonPerfectLocations.AddRange(new[] { 9d, 19, 29, 39, 59, 69, 79, 89, 99 });
|
||||
MissLocations.AddRange(new[] { 24d, 49 });
|
||||
});
|
||||
AddSliderStep("adjust score multiplier", 0, 10, (int)scoreMultiplier.Default, multiplier => scoreMultiplier.Value = multiplier);
|
||||
}
|
||||
|
||||
private const int base_great = 300;
|
||||
private const int base_ok = 100;
|
||||
|
||||
private class ScoreV1 : IScoringAlgorithm
|
||||
{
|
||||
private int currentCombo;
|
||||
|
||||
public BindableDouble ScoreMultiplier { get; } = new BindableDouble();
|
||||
|
||||
public void ApplyHit() => applyHitV1(base_great);
|
||||
public void ApplyNonPerfect() => applyHitV1(base_ok);
|
||||
public void ApplyMiss() => applyHitV1(0);
|
||||
|
||||
private void applyHitV1(int baseScore)
|
||||
{
|
||||
if (baseScore == 0)
|
||||
{
|
||||
currentCombo = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
TotalScore += baseScore;
|
||||
|
||||
// combo multiplier
|
||||
// ReSharper disable once PossibleLossOfFraction
|
||||
TotalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * ScoreMultiplier.Value));
|
||||
|
||||
currentCombo++;
|
||||
}
|
||||
|
||||
public long TotalScore { get; private set; }
|
||||
}
|
||||
|
||||
private class ScoreV2 : IScoringAlgorithm
|
||||
{
|
||||
private int currentCombo;
|
||||
private double comboPortion;
|
||||
private double currentBaseScore;
|
||||
private double maxBaseScore;
|
||||
private int currentHits;
|
||||
|
||||
private readonly double comboPortionMax;
|
||||
private readonly int maxCombo;
|
||||
|
||||
public ScoreV2(int maxCombo)
|
||||
{
|
||||
this.maxCombo = maxCombo;
|
||||
|
||||
for (int i = 0; i < this.maxCombo; i++)
|
||||
ApplyHit();
|
||||
|
||||
comboPortionMax = comboPortion;
|
||||
|
||||
currentCombo = 0;
|
||||
comboPortion = 0;
|
||||
currentBaseScore = 0;
|
||||
maxBaseScore = 0;
|
||||
currentHits = 0;
|
||||
}
|
||||
|
||||
public void ApplyHit() => applyHitV2(base_great);
|
||||
public void ApplyNonPerfect() => applyHitV2(base_ok);
|
||||
|
||||
private void applyHitV2(int baseScore)
|
||||
{
|
||||
maxBaseScore += base_great;
|
||||
currentBaseScore += baseScore;
|
||||
comboPortion += baseScore * (1 + ++currentCombo / 10.0);
|
||||
|
||||
currentHits++;
|
||||
}
|
||||
|
||||
public void ApplyMiss()
|
||||
{
|
||||
currentHits++;
|
||||
maxBaseScore += base_great;
|
||||
currentCombo = 0;
|
||||
}
|
||||
|
||||
public long TotalScore
|
||||
{
|
||||
get
|
||||
{
|
||||
double accuracy = currentBaseScore / maxBaseScore;
|
||||
|
||||
return (int)Math.Round
|
||||
(
|
||||
700000 * comboPortion / comboPortionMax +
|
||||
300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class OsuProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm
|
||||
{
|
||||
public OsuProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode)
|
||||
: base(beatmap, mode)
|
||||
{
|
||||
}
|
||||
|
||||
protected override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor();
|
||||
protected override JudgementResult CreatePerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great };
|
||||
protected override JudgementResult CreateNonPerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok };
|
||||
protected override JudgementResult CreateMissJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss };
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Audio;
|
||||
@ -19,7 +20,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
@ -35,16 +35,24 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
private int depthIndex;
|
||||
|
||||
private readonly BindableBool snakingIn = new BindableBool();
|
||||
private readonly BindableBool snakingOut = new BindableBool();
|
||||
private readonly BindableBool snakingIn = new BindableBool(true);
|
||||
private readonly BindableBool snakingOut = new BindableBool(true);
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
private float progressToHit;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
AddToggleStep("toggle snaking", v =>
|
||||
base.LoadComplete();
|
||||
|
||||
AddToggleStep("disable snaking", v =>
|
||||
{
|
||||
snakingIn.Value = v;
|
||||
snakingOut.Value = v;
|
||||
snakingIn.Value = !v;
|
||||
snakingOut.Value = !v;
|
||||
});
|
||||
|
||||
AddSliderStep("hit at", 0f, 1f, 0f, v =>
|
||||
{
|
||||
progressToHit = v;
|
||||
});
|
||||
}
|
||||
|
||||
@ -56,6 +64,18 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
foreach (var slider in this.ChildrenOfType<DrawableSlider>())
|
||||
{
|
||||
double completionProgress = Math.Clamp((Time.Current - slider.HitObject.StartTime) / slider.HitObject.Duration, 0, 1);
|
||||
if (completionProgress > progressToHit && !slider.IsHit)
|
||||
slider.HeadCircle.HitArea.Hit();
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVariousSliders()
|
||||
{
|
||||
@ -206,7 +226,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
StackHeight = 10
|
||||
};
|
||||
|
||||
return createDrawable(slider, 2, 2);
|
||||
return createDrawable(slider, 2);
|
||||
}
|
||||
|
||||
private Drawable testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats);
|
||||
@ -229,6 +249,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
SliderVelocityMultiplier = speedMultiplier,
|
||||
StartTime = Time.Current + time_offset,
|
||||
Position = new Vector2(0, -(distance / 2)),
|
||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||
@ -240,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
StackHeight = stackHeight
|
||||
};
|
||||
|
||||
return createDrawable(slider, circleSize, speedMultiplier);
|
||||
return createDrawable(slider, circleSize);
|
||||
}
|
||||
|
||||
private Drawable testPerfect(int repeats = 0)
|
||||
@ -258,7 +279,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
RepeatCount = repeats,
|
||||
};
|
||||
|
||||
return createDrawable(slider, 2, 3);
|
||||
return createDrawable(slider, 2);
|
||||
}
|
||||
|
||||
private Drawable testLinear(int repeats = 0) => createLinear(repeats);
|
||||
@ -281,7 +302,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
RepeatCount = repeats,
|
||||
};
|
||||
|
||||
return createDrawable(slider, 2, 3);
|
||||
return createDrawable(slider, 2);
|
||||
}
|
||||
|
||||
private Drawable testBezier(int repeats = 0) => createBezier(repeats);
|
||||
@ -303,7 +324,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
RepeatCount = repeats,
|
||||
};
|
||||
|
||||
return createDrawable(slider, 2, 3);
|
||||
return createDrawable(slider, 2);
|
||||
}
|
||||
|
||||
private Drawable testLinearOverlapping(int repeats = 0) => createOverlapping(repeats);
|
||||
@ -326,7 +347,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
RepeatCount = repeats,
|
||||
};
|
||||
|
||||
return createDrawable(slider, 2, 3);
|
||||
return createDrawable(slider, 2);
|
||||
}
|
||||
|
||||
private Drawable testCatmull(int repeats = 0) => createCatmull(repeats);
|
||||
@ -352,15 +373,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
NodeSamples = repeatSamples
|
||||
};
|
||||
|
||||
return createDrawable(slider, 3, 1);
|
||||
return createDrawable(slider, 3);
|
||||
}
|
||||
|
||||
private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier)
|
||||
private Drawable createDrawable(Slider slider, float circleSize)
|
||||
{
|
||||
var cpi = new LegacyControlPointInfo();
|
||||
cpi.Add(0, new DifficultyControlPoint { SliderVelocity = speedMultiplier });
|
||||
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty
|
||||
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty
|
||||
{
|
||||
CircleSize = circleSize,
|
||||
SliderTickRate = 3
|
||||
|
@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
StartTime = time_slider_start,
|
||||
Position = new Vector2(0, 0),
|
||||
SliderVelocity = velocity,
|
||||
SliderVelocityMultiplier = velocity,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
|
@ -349,7 +349,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
StartTime = time_slider_start,
|
||||
Position = new Vector2(0, 0),
|
||||
SliderVelocity = 0.1f,
|
||||
SliderVelocityMultiplier = 0.1f,
|
||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -33,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
public partial class TestSceneSliderSnaking : TestSceneOsuPlayer
|
||||
{
|
||||
[Resolved]
|
||||
private AudioManager audioManager { get; set; }
|
||||
private AudioManager audioManager { get; set; } = null!;
|
||||
|
||||
protected override bool Autoplay => autoplay;
|
||||
private bool autoplay;
|
||||
@ -41,12 +39,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
private readonly BindableBool snakingIn = new BindableBool();
|
||||
private readonly BindableBool snakingOut = new BindableBool();
|
||||
|
||||
private IBeatmap beatmap;
|
||||
private IBeatmap beatmap = null!;
|
||||
|
||||
private const double duration_of_span = 3605;
|
||||
private const double fade_in_modifier = -1200;
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
|
||||
=> new ClockBackedTestWorkingBeatmap(this.beatmap = beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -57,15 +55,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
|
||||
}
|
||||
|
||||
private Slider slider;
|
||||
private DrawableSlider drawableSlider;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
slider = null;
|
||||
drawableSlider = null;
|
||||
});
|
||||
private Slider slider = null!;
|
||||
private DrawableSlider? drawableSlider;
|
||||
|
||||
protected override bool HasCustomSteps => true;
|
||||
|
||||
@ -135,9 +126,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRepeatArrowDoesNotMoveWhenHit()
|
||||
public void TestRepeatArrowDoesNotMove([Values] bool useAutoplay)
|
||||
{
|
||||
AddStep("enable autoplay", () => autoplay = true);
|
||||
AddStep($"set autoplay to {useAutoplay}", () => autoplay = useAutoplay);
|
||||
setSnaking(true);
|
||||
CreateTest();
|
||||
// repeat might have a chance to update its position depending on where in the frame its hit,
|
||||
@ -145,21 +136,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRepeatArrowMovesWhenNotHit()
|
||||
{
|
||||
AddStep("disable autoplay", () => autoplay = false);
|
||||
setSnaking(true);
|
||||
CreateTest();
|
||||
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased);
|
||||
}
|
||||
|
||||
private void retrieveSlider(int index)
|
||||
{
|
||||
AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]);
|
||||
addSeekStep(() => slider.StartTime);
|
||||
AddUntilStep("retrieve drawable slider", () =>
|
||||
(drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
|
||||
(drawableSlider = (DrawableSlider?)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
|
||||
}
|
||||
|
||||
private void addEnsureSnakingInSteps(Func<double> startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionIncreased);
|
||||
@ -179,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
private Func<double> timeAtRepeat(Func<double> startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex;
|
||||
private Func<Vector2> positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? getSliderStart : getSliderEnd;
|
||||
|
||||
private List<Vector2> getSliderCurve() => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;
|
||||
private List<Vector2> getSliderCurve() => ((PlaySliderBody)drawableSlider!.Body.Drawable).CurrentCurve;
|
||||
private Vector2 getSliderStart() => getSliderCurve().First();
|
||||
private Vector2 getSliderEnd() => getSliderCurve().Last();
|
||||
|
||||
|
@ -336,6 +336,52 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
addJudgementAssert(hitObjects[1], HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInputFallsThroughJudgedSliders()
|
||||
{
|
||||
const double time_first_slider = 1000;
|
||||
const double time_second_slider = 1250;
|
||||
Vector2 positionFirstSlider = new Vector2(100, 50);
|
||||
Vector2 positionSecondSlider = new Vector2(100, 80);
|
||||
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
|
||||
|
||||
var hitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new TestSlider
|
||||
{
|
||||
StartTime = time_first_slider,
|
||||
Position = positionFirstSlider,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(25, 0),
|
||||
})
|
||||
},
|
||||
new TestSlider
|
||||
{
|
||||
StartTime = time_second_slider,
|
||||
Position = positionSecondSlider,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(25, 0),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
performTest(hitObjects, new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
|
||||
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint, Actions = { OsuAction.LeftButton } },
|
||||
new OsuReplayFrame { Time = time_first_slider + 50, Position = midpoint },
|
||||
});
|
||||
|
||||
addJudgementAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, HitResult.Great);
|
||||
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
|
||||
addJudgementAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Great);
|
||||
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, -200);
|
||||
}
|
||||
|
||||
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
||||
{
|
||||
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
|
||||
@ -354,6 +400,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
() => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
|
||||
}
|
||||
|
||||
private void addJudgementOffsetAssert(string name, Func<OsuHitObject> hitObject, double offset)
|
||||
{
|
||||
AddAssert($"{name} @ judged at {offset}",
|
||||
() => judgementResults.Single(r => r.HitObject == hitObject()).TimeOffset, () => Is.EqualTo(offset).Within(50));
|
||||
}
|
||||
|
||||
private ScoreAccessibleReplayPlayer currentPlayer;
|
||||
private List<JudgementResult> judgementResults;
|
||||
|
||||
@ -399,7 +451,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public TestSlider()
|
||||
{
|
||||
SliderVelocity = 0.1f;
|
||||
SliderVelocityMultiplier = 0.1f;
|
||||
|
||||
DefaultsApplied += _ =>
|
||||
{
|
||||
|
@ -1,10 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
||||
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
|
||||
GenerateTicks = generateTicksData?.GenerateTicks ?? true,
|
||||
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1,
|
||||
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1,
|
||||
}.Yield();
|
||||
|
||||
case IHasDuration endTimeData:
|
||||
|
@ -85,9 +85,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
BeginPlacement();
|
||||
|
||||
double? nearestSliderVelocity = (editorBeatmap.HitObjects
|
||||
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocity;
|
||||
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocityMultiplier;
|
||||
|
||||
HitObject.SliderVelocity = nearestSliderVelocity ?? 1;
|
||||
HitObject.SliderVelocityMultiplier = nearestSliderVelocity ?? 1;
|
||||
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
|
||||
|
||||
// Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation.
|
||||
|
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
}
|
||||
|
||||
protected override DrawableRuleset<OsuHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
protected override DrawableRuleset<OsuHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
=> new DrawableOsuEditorRuleset(ruleset, beatmap, mods);
|
||||
|
||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
||||
|
@ -61,10 +61,12 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
OsuHitObject firstObject = drawableRuleset.Beatmap.HitObjects.First();
|
||||
|
||||
// Multiplying by 2 results in an initial size that is too large, hence 1.90 has been chosen
|
||||
// Also avoids the HitObject bleeding around the edges of the bubble drawable at minimum size
|
||||
bubbleSize = (float)(drawableRuleset.Beatmap.HitObjects.OfType<HitCircle>().First().Radius * 1.90f);
|
||||
bubbleFade = drawableRuleset.Beatmap.HitObjects.OfType<HitCircle>().First().TimePreempt * 2;
|
||||
bubbleSize = (float)firstObject.Radius * 1.90f;
|
||||
bubbleFade = firstObject.TimePreempt * 2;
|
||||
|
||||
// We want to hide the judgements since they are obscured by the BubbleDrawable (due to layering)
|
||||
drawableRuleset.Playfield.DisplayJudgements.Value = false;
|
||||
|
@ -25,9 +25,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
|
||||
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);
|
||||
|
||||
[SettingSource("No slider head movement", "Pins slider heads at their starting position, regardless of time.")]
|
||||
public Bindable<bool> NoSliderHeadMovement { get; } = new BindableBool(true);
|
||||
|
||||
[SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")]
|
||||
public Bindable<bool> ClassicNoteLock { get; } = new BindableBool(true);
|
||||
|
||||
@ -71,9 +68,12 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
switch (obj)
|
||||
{
|
||||
case DrawableSliderHead head:
|
||||
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
|
||||
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
||||
applyEarlyFading(head);
|
||||
|
||||
if (ClassicNoteLock.Value)
|
||||
blockInputToObjectsUnderSliderHead(head);
|
||||
|
||||
break;
|
||||
|
||||
case DrawableSliderTail tail:
|
||||
@ -83,10 +83,27 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
case DrawableHitCircle circle:
|
||||
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
||||
applyEarlyFading(circle);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On stable, slider heads that have already been hit block input from reaching objects that may be underneath them
|
||||
/// until the sliders they're part of have been fully judged.
|
||||
/// The purpose of this method is to restore that behaviour.
|
||||
/// In order to avoid introducing yet another confusing config option, this behaviour is roped into the general notion of "note lock".
|
||||
/// </summary>
|
||||
private static void blockInputToObjectsUnderSliderHead(DrawableSliderHead slider)
|
||||
{
|
||||
var oldHitAction = slider.HitArea.Hit;
|
||||
slider.HitArea.Hit = () =>
|
||||
{
|
||||
oldHitAction?.Invoke();
|
||||
return !slider.DrawableSlider.AllJudged;
|
||||
};
|
||||
}
|
||||
|
||||
private void applyEarlyFading(DrawableHitCircle circle)
|
||||
{
|
||||
circle.ApplyCustomUpdateState += (dho, state) =>
|
||||
|
@ -2,13 +2,19 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModDifficultyAdjust : ModDifficultyAdjust
|
||||
public partial class OsuModDifficultyAdjust : ModDifficultyAdjust
|
||||
{
|
||||
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
||||
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
|
||||
@ -20,12 +26,13 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
ReadCurrentFromDifficulty = diff => diff.CircleSize,
|
||||
};
|
||||
|
||||
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
||||
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(ApproachRateSettingsControl))]
|
||||
public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
ExtendedMinValue = -10,
|
||||
ExtendedMaxValue = 11,
|
||||
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
|
||||
};
|
||||
@ -53,5 +60,34 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.Value.Value;
|
||||
if (ApproachRate.Value != null) difficulty.ApproachRate = ApproachRate.Value.Value;
|
||||
}
|
||||
|
||||
private partial class ApproachRateSettingsControl : DifficultyAdjustSettingsControl
|
||||
{
|
||||
protected override RoundedSliderBar<float> CreateSlider(BindableNumber<float> current) =>
|
||||
new ApproachRateSlider
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Current = current,
|
||||
KeyboardStep = 0.1f,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A slider bar with more detailed approach rate info for its given value
|
||||
/// </summary>
|
||||
public partial class ApproachRateSlider : RoundedSliderBar<float>
|
||||
{
|
||||
public override LocalisableString TooltipText =>
|
||||
(Current as BindableNumber<float>)?.MinValue < 0
|
||||
? $"{base.TooltipText} ({getPreemptTime(Current.Value):0} ms)"
|
||||
: base.TooltipText;
|
||||
|
||||
private double getPreemptTime(float approachRate)
|
||||
{
|
||||
var hitCircle = new HitCircle();
|
||||
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { ApproachRate = approachRate });
|
||||
return hitCircle.TimePreempt;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
ComboOffset = original.ComboOffset;
|
||||
LegacyLastTickOffset = original.LegacyLastTickOffset;
|
||||
TickDistanceMultiplier = original.TickDistanceMultiplier;
|
||||
SliderVelocity = original.SliderVelocity;
|
||||
SliderVelocityMultiplier = original.SliderVelocityMultiplier;
|
||||
}
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
|
@ -261,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
case OsuAction.RightButton:
|
||||
if (IsHovered && (Hit?.Invoke() ?? false))
|
||||
{
|
||||
HitAction = e.Action;
|
||||
HitAction ??= e.Action;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);
|
||||
|
||||
Ball.UpdateProgress(completionProgress);
|
||||
SliderBody?.UpdateProgress(completionProgress);
|
||||
SliderBody?.UpdateProgress(HeadCircle.IsHit ? completionProgress : 0);
|
||||
|
||||
foreach (DrawableHitObject hitObject in NestedHitObjects)
|
||||
{
|
||||
@ -317,7 +317,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
switch (state)
|
||||
{
|
||||
case ArmedState.Hit:
|
||||
if (SliderBody?.SnakingOut.Value == true)
|
||||
if (HeadCircle.IsHit && SliderBody?.SnakingOut.Value == true)
|
||||
Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear.
|
||||
break;
|
||||
}
|
||||
|
@ -3,11 +3,9 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
@ -24,12 +22,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult;
|
||||
|
||||
/// <summary>
|
||||
/// Makes this <see cref="DrawableSliderHead"/> track the follow circle when the start time is reached.
|
||||
/// If <c>false</c>, this <see cref="DrawableSliderHead"/> will be pinned to its initial position in the slider.
|
||||
/// </summary>
|
||||
public bool TrackFollowCircle = true;
|
||||
|
||||
private readonly IBindable<int> pathVersion = new Bindable<int>();
|
||||
|
||||
protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle;
|
||||
@ -64,23 +56,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
CheckHittable = (d, t, r) => DrawableSlider.CheckHittable?.Invoke(d, t, r) ?? ClickAction.Hit;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Debug.Assert(Slider != null);
|
||||
Debug.Assert(HitObject != null);
|
||||
|
||||
if (TrackFollowCircle)
|
||||
{
|
||||
double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1);
|
||||
|
||||
//todo: we probably want to reconsider this before adding scoring, but it looks and feels nice.
|
||||
if (!IsHit)
|
||||
Position = Slider.CurvePositionAt(completionProgress);
|
||||
}
|
||||
}
|
||||
|
||||
protected override HitResult ResultFor(double timeOffset)
|
||||
{
|
||||
Debug.Assert(HitObject != null);
|
||||
|
@ -286,7 +286,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
|
||||
|
||||
private int wholeSpins;
|
||||
private int completedFullSpins;
|
||||
|
||||
private void updateBonusScore()
|
||||
{
|
||||
@ -295,14 +295,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
int spins = (int)(Result.RateAdjustedRotation / 360);
|
||||
|
||||
if (spins < wholeSpins)
|
||||
if (spins < completedFullSpins)
|
||||
{
|
||||
// rewinding, silently handle
|
||||
wholeSpins = spins;
|
||||
completedFullSpins = spins;
|
||||
return;
|
||||
}
|
||||
|
||||
while (wholeSpins != spins)
|
||||
while (completedFullSpins != spins)
|
||||
{
|
||||
var tick = ticks.FirstOrDefault(t => !t.Result.HasResult);
|
||||
|
||||
@ -312,10 +312,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
tick.TriggerResult(true);
|
||||
|
||||
if (tick is DrawableSpinnerBonusTick)
|
||||
gainedBonus.Value = score_per_tick * (spins - HitObject.SpinsRequired);
|
||||
gainedBonus.Value = score_per_tick * (spins - HitObject.SpinsRequiredForBonus);
|
||||
}
|
||||
|
||||
wholeSpins++;
|
||||
completedFullSpins++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
@ -113,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
public double SpanDuration => Duration / this.SpanCount();
|
||||
|
||||
/// <summary>
|
||||
/// Velocity of this <see cref="Slider"/>.
|
||||
/// The computed velocity of this <see cref="Slider"/>. This is the amount of path distance travelled in 1 ms.
|
||||
/// </summary>
|
||||
public double Velocity { get; private set; }
|
||||
|
||||
@ -134,17 +135,16 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
/// </summary>
|
||||
public bool OnlyJudgeNestedObjects = true;
|
||||
|
||||
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
|
||||
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
|
||||
{
|
||||
Precision = 0.01,
|
||||
MinValue = 0.1,
|
||||
MaxValue = 10
|
||||
};
|
||||
|
||||
public double SliderVelocity
|
||||
public double SliderVelocityMultiplier
|
||||
{
|
||||
get => SliderVelocityBindable.Value;
|
||||
set => SliderVelocityBindable.Value = value;
|
||||
get => SliderVelocityMultiplierBindable.Value;
|
||||
set => SliderVelocityMultiplierBindable.Value = value;
|
||||
}
|
||||
|
||||
public bool GenerateTicks { get; set; } = true;
|
||||
@ -167,9 +167,11 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||
|
||||
double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * SliderVelocity;
|
||||
Velocity = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier / LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(this, timingPoint, OsuRuleset.SHORT_NAME);
|
||||
// WARNING: this is intentionally not computed as `BASE_SCORING_DISTANCE * difficulty.SliderMultiplier`
|
||||
// for backwards compatibility reasons (intentionally introducing floating point errors to match stable).
|
||||
double scoringDistance = Velocity * timingPoint.BeatLength;
|
||||
|
||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
||||
TickDistance = GenerateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity;
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,16 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
/// </summary>
|
||||
public int SpinsRequired { get; protected set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The number of spins required to start receiving bonus score. The first bonus is awarded on this spin count.
|
||||
/// </summary>
|
||||
public int SpinsRequiredForBonus => SpinsRequired + bonus_spins_gap;
|
||||
|
||||
/// <summary>
|
||||
/// The gap between spinner completion and the first bonus-awarding spin.
|
||||
/// </summary>
|
||||
private const int bonus_spins_gap = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Number of spins available to give bonus, beyond <see cref="SpinsRequired"/>.
|
||||
/// </summary>
|
||||
@ -42,25 +52,20 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
// spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being.
|
||||
const double stable_matching_fudge = 0.6;
|
||||
|
||||
// close to 477rpm
|
||||
const double maximum_rotations_per_second = 8;
|
||||
const double maximum_rotations_per_second = 477f / 60f;
|
||||
|
||||
double secondsDuration = Duration / 1000;
|
||||
|
||||
double minimumRotationsPerSecond = stable_matching_fudge * IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5);
|
||||
double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 1.5, 2.5, 3.75);
|
||||
|
||||
SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond);
|
||||
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration);
|
||||
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration) - bonus_spins_gap;
|
||||
}
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
{
|
||||
base.CreateNestedHitObjects(cancellationToken);
|
||||
|
||||
int totalSpins = MaximumBonusSpins + SpinsRequired;
|
||||
int totalSpins = MaximumBonusSpins + SpinsRequired + bonus_spins_gap;
|
||||
|
||||
for (int i = 0; i < totalSpins; i++)
|
||||
{
|
||||
@ -68,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
|
||||
|
||||
AddNested(i < SpinsRequired
|
||||
AddNested(i < SpinsRequiredForBonus
|
||||
? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration }
|
||||
: new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration, Samples = new[] { CreateHitSampleInfo("spinnerbonus") } });
|
||||
}
|
||||
|
@ -0,0 +1,94 @@
|
||||
{
|
||||
"Mappings": [
|
||||
{
|
||||
"StartTime": 46060.0,
|
||||
"Objects": [
|
||||
{
|
||||
"StartTime": 46060.0,
|
||||
"EndTime": 46060.0,
|
||||
"X": 160.0,
|
||||
"Y": 208.0,
|
||||
"StackOffset": {
|
||||
"X": 0.0,
|
||||
"Y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"StartTime": 46398.0,
|
||||
"EndTime": 46398.0,
|
||||
"X": 160.980164,
|
||||
"Y": 317.779083,
|
||||
"StackOffset": {
|
||||
"X": 0.0,
|
||||
"Y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"StartTime": 46737.0,
|
||||
"EndTime": 46737.0,
|
||||
"X": 268.887268,
|
||||
"Y": 320.0,
|
||||
"StackOffset": {
|
||||
"X": 0.0,
|
||||
"Y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"StartTime": 47040.0,
|
||||
"EndTime": 47040.0,
|
||||
"X": 378.995544,
|
||||
"Y": 320.0,
|
||||
"StackOffset": {
|
||||
"X": 0.0,
|
||||
"Y": 0.0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"StartTime": 123348.0,
|
||||
"Objects": [
|
||||
{
|
||||
"StartTime": 123348.0,
|
||||
"EndTime": 123348.0,
|
||||
"X": 352.0,
|
||||
"Y": 160.0,
|
||||
"StackOffset": {
|
||||
"X": 0.0,
|
||||
"Y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"StartTime": 123686.0,
|
||||
"EndTime": 123686.0,
|
||||
"X": 351.019836,
|
||||
"Y": 50.2209129,
|
||||
"StackOffset": {
|
||||
"X": 0.0,
|
||||
"Y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"StartTime": 124025.0,
|
||||
"EndTime": 124025.0,
|
||||
"X": 243.112747,
|
||||
"Y": 48.0,
|
||||
"StackOffset": {
|
||||
"X": 0.0,
|
||||
"Y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"StartTime": 124328.0,
|
||||
"EndTime": 124328.0,
|
||||
"X": 133.004471,
|
||||
"Y": 48.0,
|
||||
"StackOffset": {
|
||||
"X": 0.0,
|
||||
"Y": 0.0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
osu file format v6
|
||||
|
||||
[General]
|
||||
StackLeniency: 0.7
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:1
|
||||
CircleSize:3
|
||||
OverallDifficulty:1
|
||||
SliderMultiplier:1.1
|
||||
SliderTickRate:1
|
||||
|
||||
[TimingPoints]
|
||||
-41,338.983050847458,4,1,0,70,1,0
|
||||
93648,-100,4,1,0,70,0,0
|
||||
|
||||
[HitObjects]
|
||||
160,208,46060,6,0,B|161:320|161:320|271:320|271:320,1,330,8|0
|
||||
352,160,123348,6,0,B|351:48|351:48|241:48|241:48,1,330,8|0
|
@ -0,0 +1,39 @@
|
||||
{
|
||||
"Mappings": [
|
||||
{
|
||||
"StartTime": 7493.0,
|
||||
"Objects": [
|
||||
{
|
||||
"StartTime": 7493.0,
|
||||
"EndTime": 7493.0,
|
||||
"X": 130.0,
|
||||
"Y": 232.0,
|
||||
"StackOffset": {
|
||||
"X": 0.0,
|
||||
"Y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"StartTime": 7817.0,
|
||||
"EndTime": 7817.0,
|
||||
"X": 30.9946651,
|
||||
"Y": 208.5157,
|
||||
"StackOffset": {
|
||||
"X": 0.0,
|
||||
"Y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"StartTime": 7843.0,
|
||||
"EndTime": 7843.0,
|
||||
"X": 33.7820168,
|
||||
"Y": 208.9957,
|
||||
"StackOffset": {
|
||||
"X": 0.0,
|
||||
"Y": 0.0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
StackLeniency: 0.7
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:3
|
||||
CircleSize:3.4
|
||||
OverallDifficulty:4
|
||||
ApproachRate:5.5
|
||||
SliderMultiplier:1.1
|
||||
SliderTickRate:1
|
||||
|
||||
[Events]
|
||||
//Background and Video events
|
||||
0,0,"aa.jpg",0,0
|
||||
//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]
|
||||
6967,350.877192982456,6,2,1,55,1,0
|
||||
6967,-100,3,2,1,55,0,0
|
||||
7493,-111.111111111111,3,2,1,55,0,0
|
||||
|
||||
[HitObjects]
|
||||
130,232,7493,6,0,P|78:218|28:208,1,101.82149697876,0|0,3:2|2:0,2:0:0:0:
|
@ -46,22 +46,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
BorderSize = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1;
|
||||
BorderColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBorder)?.Value ?? Color4.White;
|
||||
|
||||
drawableObject.HitObjectApplied += onHitObjectApplied;
|
||||
}
|
||||
|
||||
private void onHitObjectApplied(DrawableHitObject obj)
|
||||
{
|
||||
var drawableSlider = (DrawableSlider)obj;
|
||||
if (drawableSlider.HitObject == null)
|
||||
return;
|
||||
|
||||
// When not tracking the follow circle, unbind from the config and forcefully disable snaking out - it looks better that way.
|
||||
if (!drawableSlider.HeadCircle.TrackFollowCircle)
|
||||
{
|
||||
SnakingOut.UnbindFrom(configSnakingOut);
|
||||
SnakingOut.Value = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour) =>
|
||||
|
@ -286,7 +286,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
if (time - part.Time >= 1)
|
||||
continue;
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)),
|
||||
TexturePosition = textureRect.BottomLeft,
|
||||
@ -295,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
Time = part.Time
|
||||
});
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)),
|
||||
TexturePosition = textureRect.BottomRight,
|
||||
@ -304,7 +304,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
Time = part.Time
|
||||
});
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y),
|
||||
TexturePosition = textureRect.TopRight,
|
||||
@ -313,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
Time = part.Time
|
||||
});
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y),
|
||||
TexturePosition = textureRect.TopLeft,
|
||||
@ -362,22 +362,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
[VertexMember(1, VertexAttribPointerType.Float)]
|
||||
public float Time;
|
||||
|
||||
[VertexMember(1, VertexAttribPointerType.Int)]
|
||||
private readonly int maskingIndex;
|
||||
|
||||
public TexturedTrailVertex(IRenderer renderer)
|
||||
{
|
||||
this = default;
|
||||
maskingIndex = renderer.CurrentMaskingIndex;
|
||||
}
|
||||
|
||||
public bool Equals(TexturedTrailVertex other)
|
||||
{
|
||||
return Position.Equals(other.Position)
|
||||
&& TexturePosition.Equals(other.TexturePosition)
|
||||
&& Colour.Equals(other.Colour)
|
||||
&& Time.Equals(other.Time)
|
||||
&& maskingIndex == other.maskingIndex;
|
||||
&& Time.Equals(other.Time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
185
osu.Game.Rulesets.Taiko.Tests/TestSceneScoring.cs
Normal file
185
osu.Game.Rulesets.Taiko.Tests/TestSceneScoring.cs
Normal file
@ -0,0 +1,185 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Judgements;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Scoring;
|
||||
using osu.Game.Tests.Visual.Gameplay;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneScoring : ScoringTestScene
|
||||
{
|
||||
private Bindable<double> scoreMultiplier { get; } = new BindableDouble
|
||||
{
|
||||
Default = 4,
|
||||
Value = 4
|
||||
};
|
||||
|
||||
protected override IBeatmap CreateBeatmap(int maxCombo)
|
||||
{
|
||||
var beatmap = new TaikoBeatmap();
|
||||
for (int i = 0; i < maxCombo; ++i)
|
||||
beatmap.HitObjects.Add(new Hit());
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1 { ScoreMultiplier = { BindTarget = scoreMultiplier } };
|
||||
protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo);
|
||||
|
||||
protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new TaikoProcessorBasedScoringAlgorithm(beatmap, mode);
|
||||
|
||||
[Test]
|
||||
public void TestBasicScenarios()
|
||||
{
|
||||
AddStep("set up score multiplier", () =>
|
||||
{
|
||||
scoreMultiplier.BindValueChanged(_ => Rerun());
|
||||
});
|
||||
AddStep("set max combo to 100", () => MaxCombo.Value = 100);
|
||||
AddStep("set perfect score", () =>
|
||||
{
|
||||
NonPerfectLocations.Clear();
|
||||
MissLocations.Clear();
|
||||
});
|
||||
AddStep("set score with misses", () =>
|
||||
{
|
||||
NonPerfectLocations.Clear();
|
||||
MissLocations.Clear();
|
||||
MissLocations.AddRange(new[] { 24d, 49 });
|
||||
});
|
||||
AddStep("set score with misses and OKs", () =>
|
||||
{
|
||||
NonPerfectLocations.Clear();
|
||||
MissLocations.Clear();
|
||||
|
||||
NonPerfectLocations.AddRange(new[] { 9d, 19, 29, 39, 59, 69, 79, 89, 99 });
|
||||
MissLocations.AddRange(new[] { 24d, 49 });
|
||||
});
|
||||
AddSliderStep("adjust score multiplier", 0, 10, (int)scoreMultiplier.Default, multiplier => scoreMultiplier.Value = multiplier);
|
||||
}
|
||||
|
||||
private const int base_great = 300;
|
||||
private const int base_ok = 150;
|
||||
|
||||
private class ScoreV1 : IScoringAlgorithm
|
||||
{
|
||||
private int currentCombo;
|
||||
|
||||
public BindableDouble ScoreMultiplier { get; } = new BindableDouble();
|
||||
|
||||
public void ApplyHit() => applyHitV1(base_great);
|
||||
|
||||
public void ApplyNonPerfect() => applyHitV1(base_ok);
|
||||
|
||||
public void ApplyMiss() => applyHitV1(0);
|
||||
|
||||
private void applyHitV1(int baseScore)
|
||||
{
|
||||
if (baseScore == 0)
|
||||
{
|
||||
currentCombo = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
TotalScore += baseScore;
|
||||
|
||||
// combo multiplier
|
||||
// ReSharper disable once PossibleLossOfFraction
|
||||
TotalScore += (int)((baseScore / 35) * 2 * (ScoreMultiplier.Value + 1)) * (Math.Min(100, currentCombo) / 10);
|
||||
|
||||
currentCombo++;
|
||||
}
|
||||
|
||||
public long TotalScore { get; private set; }
|
||||
}
|
||||
|
||||
private class ScoreV2 : IScoringAlgorithm
|
||||
{
|
||||
private int currentCombo;
|
||||
private double comboPortion;
|
||||
private double currentBaseScore;
|
||||
private double maxBaseScore;
|
||||
private int currentHits;
|
||||
|
||||
private readonly double comboPortionMax;
|
||||
private readonly int maxCombo;
|
||||
|
||||
private const double combo_base = 4;
|
||||
|
||||
public ScoreV2(int maxCombo)
|
||||
{
|
||||
this.maxCombo = maxCombo;
|
||||
|
||||
for (int i = 0; i < this.maxCombo; i++)
|
||||
ApplyHit();
|
||||
|
||||
comboPortionMax = comboPortion;
|
||||
|
||||
currentCombo = 0;
|
||||
comboPortion = 0;
|
||||
currentBaseScore = 0;
|
||||
maxBaseScore = 0;
|
||||
currentHits = 0;
|
||||
}
|
||||
|
||||
public void ApplyHit() => applyHitV2(base_great);
|
||||
|
||||
public void ApplyNonPerfect() => applyHitV2(base_ok);
|
||||
|
||||
private void applyHitV2(int baseScore)
|
||||
{
|
||||
maxBaseScore += base_great;
|
||||
currentBaseScore += baseScore;
|
||||
|
||||
currentHits++;
|
||||
|
||||
// `base_great` is INTENTIONALLY used above here instead of `baseScore`
|
||||
// see `BaseHitValue` override in `ScoreChangeTaiko` on stable
|
||||
comboPortion += base_great * Math.Min(Math.Max(0.5, Math.Log(++currentCombo, combo_base)), Math.Log(400, combo_base));
|
||||
}
|
||||
|
||||
public void ApplyMiss()
|
||||
{
|
||||
currentHits++;
|
||||
maxBaseScore += base_great;
|
||||
currentCombo = 0;
|
||||
}
|
||||
|
||||
public long TotalScore
|
||||
{
|
||||
get
|
||||
{
|
||||
double accuracy = currentBaseScore / maxBaseScore;
|
||||
|
||||
return (int)Math.Round
|
||||
(
|
||||
250000 * comboPortion / comboPortionMax +
|
||||
750000 * Math.Pow(accuracy, 3.6) * ((double)currentHits / maxCombo)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TaikoProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm
|
||||
{
|
||||
public TaikoProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode)
|
||||
: base(beatmap, mode)
|
||||
{
|
||||
}
|
||||
|
||||
protected override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor();
|
||||
protected override JudgementResult CreatePerfectJudgementResult() => new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great };
|
||||
protected override JudgementResult CreateNonPerfectJudgementResult() => new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok };
|
||||
protected override JudgementResult CreateMissJudgementResult() => new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss };
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
@ -14,6 +14,7 @@ using JetBrains.Annotations;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
{
|
||||
@ -64,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
{
|
||||
if (hitObject is not IHasSliderVelocity hasSliderVelocity) continue;
|
||||
|
||||
double nextScrollSpeed = hasSliderVelocity.SliderVelocity;
|
||||
double nextScrollSpeed = hasSliderVelocity.SliderVelocityMultiplier;
|
||||
EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime);
|
||||
|
||||
if (!Precision.AlmostEquals(lastScrollSpeed, nextScrollSpeed, acceptableDifference: currentEffectPoint.ScrollSpeedBindable.Precision))
|
||||
@ -186,10 +187,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
|
||||
|
||||
double beatLength;
|
||||
if (obj.LegacyBpmMultiplier.HasValue)
|
||||
beatLength = timingPoint.BeatLength * obj.LegacyBpmMultiplier.Value;
|
||||
else if (obj is IHasSliderVelocity hasSliderVelocity)
|
||||
beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
|
||||
|
||||
if (obj is IHasSliderVelocity hasSliderVelocity)
|
||||
beatLength = LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(hasSliderVelocity, timingPoint, TaikoRuleset.SHORT_NAME);
|
||||
else
|
||||
beatLength = timingPoint.BeatLength;
|
||||
|
||||
|
37
osu.Game.Rulesets.Taiko/Edit/DrawableTaikoEditorRuleset.cs
Normal file
37
osu.Game.Rulesets.Taiko/Edit/DrawableTaikoEditorRuleset.cs
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
public partial class DrawableTaikoEditorRuleset : DrawableTaikoRuleset, ISupportConstantAlgorithmToggle
|
||||
{
|
||||
public BindableBool ShowSpeedChanges { get; set; } = new BindableBool();
|
||||
|
||||
public DrawableTaikoEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
: base(ruleset, beatmap, mods)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Overlapping : ScrollVisualisationMethod.Constant, true);
|
||||
}
|
||||
|
||||
protected override double ComputeTimeRange()
|
||||
{
|
||||
// Adjust when we're using constant algorithm to not be sluggish.
|
||||
double multiplier = ShowSpeedChanges.Value ? 1 : 4;
|
||||
return base.ComputeTimeRange() / multiplier;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,14 +2,17 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
public partial class TaikoHitObjectComposer : HitObjectComposer<TaikoHitObject>
|
||||
public partial class TaikoHitObjectComposer : ScrollingHitObjectComposer<TaikoHitObject>
|
||||
{
|
||||
protected override bool ApplyHorizontalCentering => false;
|
||||
|
||||
@ -25,6 +28,9 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
new SwellCompositionTool()
|
||||
};
|
||||
|
||||
protected override DrawableRuleset<TaikoHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods) =>
|
||||
new DrawableTaikoEditorRuleset(ruleset, beatmap, mods);
|
||||
|
||||
protected override ComposeBlueprintContainer CreateBlueprintContainer()
|
||||
=> new TaikoBlueprintContainer(this);
|
||||
}
|
||||
|
@ -35,8 +35,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
|
||||
|
||||
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
|
||||
|
||||
protected override bool UserScrollSpeedAdjustment => false;
|
||||
|
||||
private SkinnableDrawable scroller;
|
||||
@ -45,6 +43,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
: base(ruleset, beatmap, mods)
|
||||
{
|
||||
Direction.Value = ScrollingDirection.Left;
|
||||
VisualisationMethod = ScrollVisualisationMethod.Overlapping;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -65,6 +64,11 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
{
|
||||
base.Update();
|
||||
|
||||
TimeRange.Value = ComputeTimeRange();
|
||||
}
|
||||
|
||||
protected virtual double ComputeTimeRange()
|
||||
{
|
||||
// Taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened.
|
||||
const float scroll_rate = 10;
|
||||
|
||||
@ -73,7 +77,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
// We clamp the ratio to the maximum aspect ratio to keep scroll speed consistent on widths lower than the default.
|
||||
float ratio = Math.Max(DrawSize.X / 768f, TaikoPlayfieldAdjustmentContainer.MAXIMUM_ASPECT);
|
||||
|
||||
TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
|
||||
return (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
|
@ -621,6 +621,38 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInvalidBankDefaultsToNormal()
|
||||
{
|
||||
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||
|
||||
using (var resStream = TestResources.OpenResource("invalid-bank.osu"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var hitObjects = decoder.Decode(stream).HitObjects;
|
||||
|
||||
assertObjectHasBanks(hitObjects[0], HitSampleInfo.BANK_DRUM);
|
||||
assertObjectHasBanks(hitObjects[1], HitSampleInfo.BANK_NORMAL);
|
||||
assertObjectHasBanks(hitObjects[2], HitSampleInfo.BANK_SOFT);
|
||||
assertObjectHasBanks(hitObjects[3], HitSampleInfo.BANK_DRUM);
|
||||
assertObjectHasBanks(hitObjects[4], HitSampleInfo.BANK_NORMAL);
|
||||
|
||||
assertObjectHasBanks(hitObjects[5], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_DRUM);
|
||||
assertObjectHasBanks(hitObjects[6], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_NORMAL);
|
||||
assertObjectHasBanks(hitObjects[7], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_SOFT);
|
||||
assertObjectHasBanks(hitObjects[8], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_DRUM);
|
||||
assertObjectHasBanks(hitObjects[9], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_NORMAL);
|
||||
}
|
||||
|
||||
void assertObjectHasBanks(HitObject hitObject, string normalBank, string? additionsBank = null)
|
||||
{
|
||||
Assert.AreEqual(normalBank, hitObject.Samples[0].Bank);
|
||||
|
||||
if (additionsBank != null)
|
||||
Assert.AreEqual(additionsBank, hitObject.Samples[1].Bank);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFallbackDecoderForCorruptedHeader()
|
||||
{
|
||||
@ -1024,10 +1056,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
Assert.That(controlPoints.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1));
|
||||
Assert.That(controlPoints.DifficultyPointAt(3000).SliderVelocity, Is.EqualTo(1));
|
||||
|
||||
#pragma warning disable 618
|
||||
Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(2000)).GenerateTicks, Is.False);
|
||||
Assert.That(((LegacyBeatmapDecoder.LegacyDifficultyControlPoint)controlPoints.DifficultyPointAt(3000)).GenerateTicks, Is.True);
|
||||
#pragma warning restore 618
|
||||
Assert.That(controlPoints.DifficultyPointAt(2000).GenerateTicks, Is.False);
|
||||
Assert.That(controlPoints.DifficultyPointAt(3000).GenerateTicks, Is.True);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,8 +32,26 @@ namespace osu.Game.Tests.Chat
|
||||
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a gopher://really-old-protocol we don't support." });
|
||||
|
||||
Assert.AreEqual(result.Content, result.DisplayContent);
|
||||
Assert.AreEqual(1, result.Links.Count);
|
||||
Assert.AreEqual("gopher://really-old-protocol", result.Links[0].Url);
|
||||
Assert.AreEqual(0, result.Links.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFakeProtocolLink()
|
||||
{
|
||||
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a osunotarealprotocol://completely-made-up-protocol we don't support." });
|
||||
|
||||
Assert.AreEqual(result.Content, result.DisplayContent);
|
||||
Assert.AreEqual(0, result.Links.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSupportedProtocolLinkParsing()
|
||||
{
|
||||
Message result = MessageFormatter.FormatMessage(new Message { Content = "forgotspacehttps://dev.ppy.sh joinmyosump://12345 jointheosu://chan/#english" });
|
||||
|
||||
Assert.AreEqual("https://dev.ppy.sh", result.Links[0].Url);
|
||||
Assert.AreEqual("osump://12345", result.Links[1].Url);
|
||||
Assert.AreEqual("osu://chan/#english", result.Links[2].Url);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -78,7 +78,7 @@ namespace osu.Game.Tests.Editing
|
||||
{
|
||||
assertSnapDistance(100, new Slider
|
||||
{
|
||||
SliderVelocity = multiplier
|
||||
SliderVelocityMultiplier = multiplier
|
||||
}, false);
|
||||
}
|
||||
|
||||
@ -88,7 +88,7 @@ namespace osu.Game.Tests.Editing
|
||||
{
|
||||
assertSnapDistance(100 * multiplier, new Slider
|
||||
{
|
||||
SliderVelocity = multiplier
|
||||
SliderVelocityMultiplier = multiplier
|
||||
}, true);
|
||||
}
|
||||
|
||||
@ -112,7 +112,7 @@ namespace osu.Game.Tests.Editing
|
||||
|
||||
var referenceObject = new Slider
|
||||
{
|
||||
SliderVelocity = slider_velocity
|
||||
SliderVelocityMultiplier = slider_velocity
|
||||
};
|
||||
|
||||
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
|
||||
|
@ -13,7 +13,7 @@ layout(location = 4) out mediump vec2 v_BlendRange;
|
||||
void main(void)
|
||||
{
|
||||
// Transform from screen space to masking space.
|
||||
highp vec3 maskingPos = g_MaskingInfo.ToMaskingSpace * vec3(m_Position, 1.0);
|
||||
highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
|
||||
v_MaskingPosition = maskingPos.xy / maskingPos.z;
|
||||
|
||||
v_Colour = m_Colour;
|
||||
|
19
osu.Game.Tests/Resources/invalid-bank.osu
Normal file
19
osu.Game.Tests/Resources/invalid-bank.osu
Normal file
@ -0,0 +1,19 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
SampleSet: Normal
|
||||
|
||||
[TimingPoints]
|
||||
0,500,4,3,0,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
256,192,1000,5,0,0:0:0:0:
|
||||
256,192,2000,1,0,1:0:0:0:
|
||||
256,192,3000,1,0,2:0:0:0:
|
||||
256,192,4000,1,0,3:0:0:0:
|
||||
256,192,5000,1,0,42:0:0:0:
|
||||
256,192,6000,5,4,0:0:0:0:
|
||||
256,192,7000,1,4,0:1:0:0:
|
||||
256,192,8000,1,4,0:2:0:0:
|
||||
256,192,9000,1,4,0:3:0:0:
|
||||
256,192,10000,1,4,0:42:0:0:
|
@ -260,6 +260,12 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
||||
AddStep($"set {scheme} scheme", () => Child = createContent(scheme, creationFunc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNano()
|
||||
{
|
||||
createTestCase(beatmapSetInfo => new BeatmapCardNano(beatmapSetInfo));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNormal()
|
||||
{
|
||||
|
@ -47,7 +47,35 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
||||
pill.AutoSizeAxes = Axes.Y;
|
||||
pill.Width = 90;
|
||||
}));
|
||||
|
||||
AddStep("unset fixed width", () => statusPills.ForEach(pill => pill.AutoSizeAxes = Axes.Both));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeLabels()
|
||||
{
|
||||
AddStep("Change labels", () =>
|
||||
{
|
||||
foreach (var pill in this.ChildrenOfType<BeatmapSetOnlineStatusPill>())
|
||||
{
|
||||
switch (pill.Status)
|
||||
{
|
||||
// cycle at end
|
||||
case BeatmapOnlineStatus.Loved:
|
||||
pill.Status = BeatmapOnlineStatus.LocallyModified;
|
||||
break;
|
||||
|
||||
// skip none
|
||||
case BeatmapOnlineStatus.LocallyModified:
|
||||
pill.Status = BeatmapOnlineStatus.Graveyard;
|
||||
break;
|
||||
|
||||
default:
|
||||
pill.Status = (pill.Status + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
new PathControlPoint(new Vector2(100, 0))
|
||||
}
|
||||
},
|
||||
SliderVelocity = 2
|
||||
SliderVelocityMultiplier = 2
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("unify slider velocity", () =>
|
||||
{
|
||||
foreach (var h in EditorBeatmap.HitObjects.OfType<IHasSliderVelocity>())
|
||||
h.SliderVelocity = 1.5;
|
||||
h.SliderVelocityMultiplier = 1.5;
|
||||
});
|
||||
|
||||
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
@ -194,7 +194,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () =>
|
||||
{
|
||||
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
|
||||
return h is IHasSliderVelocity hasSliderVelocity && hasSliderVelocity.SliderVelocity == velocity;
|
||||
return h is IHasSliderVelocity hasSliderVelocity && hasSliderVelocity.SliderVelocityMultiplier == velocity;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -67,6 +67,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private Player loadPlayerFor(RulesetInfo rulesetInfo)
|
||||
{
|
||||
// if a player screen is present already, we must exit that before loading another one,
|
||||
// otherwise it'll crash on SpectatorClient.BeginPlaying being called while client is in "playing" state already.
|
||||
if (Stack.CurrentScreen is Player)
|
||||
Stack.Exit();
|
||||
|
||||
Ruleset.Value = rulesetInfo;
|
||||
var ruleset = rulesetInfo.CreateInstance();
|
||||
|
||||
|
@ -311,14 +311,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
protected override bool RelativeScaleBeatLengths => RelativeScaleBeatLengthsOverride;
|
||||
|
||||
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
|
||||
|
||||
public new Bindable<double> TimeRange => base.TimeRange;
|
||||
|
||||
public TestDrawableScrollingRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
: base(ruleset, beatmap, mods)
|
||||
{
|
||||
TimeRange.Value = time_range;
|
||||
VisualisationMethod = ScrollVisualisationMethod.Overlapping;
|
||||
}
|
||||
|
||||
public override DrawableHitObject<TestHitObject> CreateDrawableRepresentation(TestHitObject h)
|
||||
|
@ -3,14 +3,17 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.PolygonExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osuTK;
|
||||
@ -139,6 +142,37 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
=> AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (GameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFriendScore()
|
||||
{
|
||||
APIUser friend = new APIUser { Username = "my friend", Id = 10000 };
|
||||
|
||||
createLeaderboard();
|
||||
addLocalPlayer();
|
||||
|
||||
AddStep("Add friend to API", () =>
|
||||
{
|
||||
var api = (DummyAPIAccess)API;
|
||||
|
||||
api.Friends.Clear();
|
||||
api.Friends.Add(friend);
|
||||
});
|
||||
|
||||
int playerNumber = 1;
|
||||
|
||||
AddRepeatStep("add 3 other players", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 3);
|
||||
AddUntilStep("no pink color scores",
|
||||
() => leaderboard.ChildrenOfType<Box>().Select(b => ((Colour4)b.Colour).ToHex()),
|
||||
() => Does.Not.Contain("#FF549A"));
|
||||
|
||||
AddRepeatStep("add 3 friend score", () => createRandomScore(friend), 3);
|
||||
AddUntilStep("at least one friend score is pink",
|
||||
() => leaderboard.GetAllScoresForUsername("my friend")
|
||||
.SelectMany(score => score.ChildrenOfType<Box>())
|
||||
.Select(b => ((Colour4)b.Colour).ToHex()),
|
||||
() => Does.Contain("#FF549A"));
|
||||
}
|
||||
|
||||
private void addLocalPlayer()
|
||||
{
|
||||
AddStep("add local player", () =>
|
||||
@ -179,6 +213,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
return scoreItem != null && scoreItem.ScorePosition == expectedPosition;
|
||||
}
|
||||
|
||||
public IEnumerable<GameplayLeaderboardScore> GetAllScoresForUsername(string username)
|
||||
=> Flow.Where(i => i.User?.Username == username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,499 +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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneScoring : OsuTestScene
|
||||
{
|
||||
private GraphContainer graphs = null!;
|
||||
private SettingsSlider<int> sliderMaxCombo = null!;
|
||||
|
||||
private FillFlowContainer legend = null!;
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddStep("setup tests", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
graphs = new GraphContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
legend = new FillFlowContainer
|
||||
{
|
||||
Padding = new MarginPadding(20),
|
||||
Direction = FillDirection.Full,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
},
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
Padding = new MarginPadding(20),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Full,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
sliderMaxCombo = new SettingsSlider<int>
|
||||
{
|
||||
Width = 0.5f,
|
||||
TransferValueOnCommit = true,
|
||||
Current = new BindableInt(1024)
|
||||
{
|
||||
MinValue = 96,
|
||||
MaxValue = 8192,
|
||||
},
|
||||
LabelText = "max combo",
|
||||
},
|
||||
new OsuTextFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.5f,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Text = $"Left click to add miss\nRight click to add OK/{base_ok}"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sliderMaxCombo.Current.BindValueChanged(_ => rerun());
|
||||
|
||||
graphs.MissLocations.BindCollectionChanged((_, __) => rerun());
|
||||
graphs.NonPerfectLocations.BindCollectionChanged((_, __) => rerun());
|
||||
|
||||
graphs.MaxCombo.BindTo(sliderMaxCombo.Current);
|
||||
|
||||
rerun();
|
||||
});
|
||||
}
|
||||
|
||||
private const int base_great = 300;
|
||||
private const int base_ok = 100;
|
||||
|
||||
private void rerun()
|
||||
{
|
||||
graphs.Clear();
|
||||
legend.Clear();
|
||||
|
||||
runForProcessor("lazer-standardised", Color4.YellowGreen, new OsuScoreProcessor(), ScoringMode.Standardised);
|
||||
runForProcessor("lazer-classic", Color4.MediumPurple, new OsuScoreProcessor(), ScoringMode.Classic);
|
||||
|
||||
runScoreV1();
|
||||
runScoreV2();
|
||||
}
|
||||
|
||||
private void runScoreV1()
|
||||
{
|
||||
int totalScore = 0;
|
||||
int currentCombo = 0;
|
||||
|
||||
void applyHitV1(int baseScore)
|
||||
{
|
||||
if (baseScore == 0)
|
||||
{
|
||||
currentCombo = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const float score_multiplier = 1;
|
||||
|
||||
totalScore += baseScore;
|
||||
|
||||
// combo multiplier
|
||||
// ReSharper disable once PossibleLossOfFraction
|
||||
totalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * score_multiplier));
|
||||
|
||||
currentCombo++;
|
||||
}
|
||||
|
||||
runForAlgorithm("ScoreV1 (classic)", Color4.Purple,
|
||||
() => applyHitV1(base_great),
|
||||
() => applyHitV1(base_ok),
|
||||
() => applyHitV1(0),
|
||||
() =>
|
||||
{
|
||||
// Arbitrary value chosen towards the upper range.
|
||||
const double score_multiplier = 4;
|
||||
|
||||
return (int)(totalScore * score_multiplier);
|
||||
});
|
||||
}
|
||||
|
||||
private void runScoreV2()
|
||||
{
|
||||
int maxCombo = sliderMaxCombo.Current.Value;
|
||||
|
||||
int currentCombo = 0;
|
||||
double comboPortion = 0;
|
||||
double currentBaseScore = 0;
|
||||
double maxBaseScore = 0;
|
||||
int currentHits = 0;
|
||||
|
||||
for (int i = 0; i < maxCombo; i++)
|
||||
applyHitV2(base_great);
|
||||
|
||||
double comboPortionMax = comboPortion;
|
||||
|
||||
currentCombo = 0;
|
||||
comboPortion = 0;
|
||||
currentBaseScore = 0;
|
||||
maxBaseScore = 0;
|
||||
currentHits = 0;
|
||||
|
||||
void applyHitV2(int baseScore)
|
||||
{
|
||||
maxBaseScore += base_great;
|
||||
currentBaseScore += baseScore;
|
||||
comboPortion += baseScore * (1 + ++currentCombo / 10.0);
|
||||
|
||||
currentHits++;
|
||||
}
|
||||
|
||||
runForAlgorithm("ScoreV2", Color4.OrangeRed,
|
||||
() => applyHitV2(base_great),
|
||||
() => applyHitV2(base_ok),
|
||||
() =>
|
||||
{
|
||||
currentHits++;
|
||||
maxBaseScore += base_great;
|
||||
currentCombo = 0;
|
||||
}, () =>
|
||||
{
|
||||
double accuracy = currentBaseScore / maxBaseScore;
|
||||
|
||||
return (int)Math.Round
|
||||
(
|
||||
700000 * comboPortion / comboPortionMax +
|
||||
300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private void runForProcessor(string name, Color4 colour, ScoreProcessor processor, ScoringMode mode)
|
||||
{
|
||||
int maxCombo = sliderMaxCombo.Current.Value;
|
||||
|
||||
var beatmap = new OsuBeatmap();
|
||||
for (int i = 0; i < maxCombo; i++)
|
||||
beatmap.HitObjects.Add(new HitCircle());
|
||||
|
||||
processor.ApplyBeatmap(beatmap);
|
||||
|
||||
runForAlgorithm(name, colour,
|
||||
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }),
|
||||
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }),
|
||||
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }),
|
||||
() => processor.GetDisplayScore(mode));
|
||||
}
|
||||
|
||||
private void runForAlgorithm(string name, Color4 colour, Action applyHit, Action applyNonPerfect, Action applyMiss, Func<long> getTotalScore)
|
||||
{
|
||||
int maxCombo = sliderMaxCombo.Current.Value;
|
||||
|
||||
List<float> results = new List<float>();
|
||||
|
||||
for (int i = 0; i < maxCombo; i++)
|
||||
{
|
||||
if (graphs.MissLocations.Contains(i))
|
||||
applyMiss();
|
||||
else if (graphs.NonPerfectLocations.Contains(i))
|
||||
applyNonPerfect();
|
||||
else
|
||||
applyHit();
|
||||
|
||||
results.Add(getTotalScore());
|
||||
}
|
||||
|
||||
graphs.Add(new LineGraph
|
||||
{
|
||||
Name = name,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
LineColour = colour,
|
||||
Values = results
|
||||
});
|
||||
|
||||
legend.Add(new OsuSpriteText
|
||||
{
|
||||
Colour = colour,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.5f,
|
||||
Text = $"{FontAwesome.Solid.Circle.Icon} {name}"
|
||||
});
|
||||
|
||||
legend.Add(new OsuSpriteText
|
||||
{
|
||||
Colour = colour,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.5f,
|
||||
Text = $"final score {getTotalScore():#,0}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public partial class GraphContainer : Container, IHasCustomTooltip<IEnumerable<LineGraph>>
|
||||
{
|
||||
public readonly BindableList<double> MissLocations = new BindableList<double>();
|
||||
public readonly BindableList<double> NonPerfectLocations = new BindableList<double>();
|
||||
|
||||
public Bindable<int> MaxCombo = new Bindable<int>();
|
||||
|
||||
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
private readonly Box hoverLine;
|
||||
|
||||
private readonly Container missLines;
|
||||
private readonly Container verticalGridLines;
|
||||
|
||||
public int CurrentHoverCombo { get; private set; }
|
||||
|
||||
public GraphContainer()
|
||||
{
|
||||
InternalChild = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = OsuColour.Gray(0.1f),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
verticalGridLines = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
hoverLine = new Box
|
||||
{
|
||||
Colour = Color4.Yellow,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Origin = Anchor.TopCentre,
|
||||
Alpha = 0,
|
||||
Width = 1,
|
||||
},
|
||||
missLines = new Container
|
||||
{
|
||||
Alpha = 0.6f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
Content,
|
||||
}
|
||||
};
|
||||
|
||||
MissLocations.BindCollectionChanged((_, _) => updateMissLocations());
|
||||
NonPerfectLocations.BindCollectionChanged((_, _) => updateMissLocations());
|
||||
|
||||
MaxCombo.BindValueChanged(_ =>
|
||||
{
|
||||
updateMissLocations();
|
||||
updateVerticalGridLines();
|
||||
}, true);
|
||||
}
|
||||
|
||||
private void updateVerticalGridLines()
|
||||
{
|
||||
verticalGridLines.Clear();
|
||||
|
||||
for (int i = 0; i < MaxCombo.Value; i++)
|
||||
{
|
||||
if (i % 100 == 0)
|
||||
{
|
||||
verticalGridLines.AddRange(new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = OsuColour.Gray(0.2f),
|
||||
Origin = Anchor.TopCentre,
|
||||
Width = 1,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
RelativePositionAxes = Axes.X,
|
||||
X = (float)i / MaxCombo.Value,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
RelativePositionAxes = Axes.X,
|
||||
X = (float)i / MaxCombo.Value,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Text = $"{i:#,0}",
|
||||
Rotation = -30,
|
||||
Y = -20,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMissLocations()
|
||||
{
|
||||
missLines.Clear();
|
||||
|
||||
foreach (int miss in MissLocations)
|
||||
{
|
||||
missLines.Add(new Box
|
||||
{
|
||||
Colour = Color4.Red,
|
||||
Origin = Anchor.TopCentre,
|
||||
Width = 1,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
RelativePositionAxes = Axes.X,
|
||||
X = (float)miss / MaxCombo.Value,
|
||||
});
|
||||
}
|
||||
|
||||
foreach (int miss in NonPerfectLocations)
|
||||
{
|
||||
missLines.Add(new Box
|
||||
{
|
||||
Colour = Color4.Orange,
|
||||
Origin = Anchor.TopCentre,
|
||||
Width = 1,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
RelativePositionAxes = Axes.X,
|
||||
X = (float)miss / MaxCombo.Value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
hoverLine.Show();
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
hoverLine.Hide();
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
{
|
||||
CurrentHoverCombo = (int)(e.MousePosition.X / DrawWidth * MaxCombo.Value);
|
||||
|
||||
hoverLine.X = e.MousePosition.X;
|
||||
return base.OnMouseMove(e);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
if (e.Button == MouseButton.Left)
|
||||
MissLocations.Add(CurrentHoverCombo);
|
||||
else
|
||||
NonPerfectLocations.Add(CurrentHoverCombo);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private GraphTooltip? tooltip;
|
||||
|
||||
public ITooltip<IEnumerable<LineGraph>> GetCustomTooltip() => tooltip ??= new GraphTooltip(this);
|
||||
|
||||
public IEnumerable<LineGraph> TooltipContent => Content.OfType<LineGraph>();
|
||||
|
||||
public partial class GraphTooltip : CompositeDrawable, ITooltip<IEnumerable<LineGraph>>
|
||||
{
|
||||
private readonly GraphContainer graphContainer;
|
||||
|
||||
private readonly OsuTextFlowContainer textFlow;
|
||||
|
||||
public GraphTooltip(GraphContainer graphContainer)
|
||||
{
|
||||
this.graphContainer = graphContainer;
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
Masking = true;
|
||||
CornerRadius = 10;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = OsuColour.Gray(0.15f),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
textFlow = new OsuTextFlowContainer
|
||||
{
|
||||
Colour = Color4.White,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(10),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private int? lastContentCombo;
|
||||
|
||||
public void SetContent(IEnumerable<LineGraph> content)
|
||||
{
|
||||
int relevantCombo = graphContainer.CurrentHoverCombo;
|
||||
|
||||
if (lastContentCombo == relevantCombo)
|
||||
return;
|
||||
|
||||
lastContentCombo = relevantCombo;
|
||||
textFlow.Clear();
|
||||
|
||||
textFlow.AddParagraph($"At combo {relevantCombo}:");
|
||||
|
||||
foreach (var graph in content)
|
||||
{
|
||||
float valueAtHover = graph.Values.ElementAt(relevantCombo);
|
||||
float ofTotal = valueAtHover / graph.Values.Last();
|
||||
|
||||
textFlow.AddParagraph($"{graph.Name}: {valueAtHover:#,0} ({ofTotal * 100:N0}% of final)\n", st => st.Colour = graph.LineColour);
|
||||
}
|
||||
}
|
||||
|
||||
public void Move(Vector2 pos) => this.MoveTo(pos);
|
||||
}
|
||||
}
|
||||
}
|
@ -64,6 +64,9 @@ namespace osu.Game.Tests.Visual.Online
|
||||
addMessageWithChecks("test!");
|
||||
addMessageWithChecks("dev.ppy.sh!");
|
||||
addMessageWithChecks("https://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
|
||||
addMessageWithChecks("http://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
|
||||
addMessageWithChecks("forgothttps://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
|
||||
addMessageWithChecks("forgothttp://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
|
||||
addMessageWithChecks("00:12:345 (1,2) - Test?", 1, expectedActions: LinkAction.OpenEditorTimestamp);
|
||||
addMessageWithChecks("Wiki link for tasty [[Performance Points]]", 1, expectedActions: LinkAction.OpenWiki);
|
||||
addMessageWithChecks("(osu forums)[https://dev.ppy.sh/forum] (old link format)", 1, expectedActions: LinkAction.External);
|
||||
@ -84,9 +87,11 @@ namespace osu.Game.Tests.Visual.Online
|
||||
addMessageWithChecks("feels important", 0, true, true);
|
||||
addMessageWithChecks("likes to post this [https://dev.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External);
|
||||
addMessageWithChecks("Join my multiplayer game osump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
|
||||
addMessageWithChecks("Join my multiplayer gameosump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
|
||||
addMessageWithChecks("Join my [multiplayer game](osump://12346).", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
|
||||
addMessageWithChecks($"Join my [#english]({OsuGameBase.OSU_PROTOCOL}chan/#english).", 1, expectedActions: LinkAction.OpenChannel);
|
||||
addMessageWithChecks($"Join my {OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel);
|
||||
addMessageWithChecks($"Join my{OsuGameBase.OSU_PROTOCOL}chan/#english.", 1, expectedActions: LinkAction.OpenChannel);
|
||||
addMessageWithChecks("Join my #english or #japanese channels.", 2, expectedActions: new[] { LinkAction.OpenChannel, LinkAction.OpenChannel });
|
||||
addMessageWithChecks("Join my #english or #nonexistent #hashtag channels.", 1, expectedActions: LinkAction.OpenChannel);
|
||||
addMessageWithChecks("Hello world\uD83D\uDE12(<--This is an emoji). There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20");
|
||||
|
@ -0,0 +1,97 @@
|
||||
// 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 System.Net;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
public partial class TestSceneReplayMissingBeatmap : OsuGameTestScene
|
||||
{
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
|
||||
[Test]
|
||||
public void TestSceneMissingBeatmapWithOnlineAvailable()
|
||||
{
|
||||
var beatmap = new APIBeatmap
|
||||
{
|
||||
OnlineBeatmapSetID = 173612,
|
||||
BeatmapSet = new APIBeatmapSet
|
||||
{
|
||||
Title = "FREEDOM Dive",
|
||||
Artist = "xi",
|
||||
Covers = new BeatmapSetOnlineCovers
|
||||
{
|
||||
Card = "https://assets.ppy.sh/beatmaps/173612/covers/card@2x.jpg"
|
||||
},
|
||||
OnlineID = 173612
|
||||
}
|
||||
};
|
||||
|
||||
setupBeatmapResponse(beatmap);
|
||||
|
||||
AddStep("import score", () =>
|
||||
{
|
||||
using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
|
||||
{
|
||||
var importTask = new ImportTask(resourceStream, "replay.osr");
|
||||
|
||||
Game.ScoreManager.Import(new[] { importTask });
|
||||
}
|
||||
});
|
||||
|
||||
AddUntilStep("Replay missing notification show", () => Game.Notifications.ChildrenOfType<MissingBeatmapNotification>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSceneMissingBeatmapWithOnlineUnavailable()
|
||||
{
|
||||
setupFailedResponse();
|
||||
|
||||
AddStep("import score", () =>
|
||||
{
|
||||
using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
|
||||
{
|
||||
var importTask = new ImportTask(resourceStream, "replay.osr");
|
||||
|
||||
Game.ScoreManager.Import(new[] { importTask });
|
||||
}
|
||||
});
|
||||
|
||||
AddUntilStep("Replay missing notification not show", () => !Game.Notifications.ChildrenOfType<MissingBeatmapNotification>().Any());
|
||||
}
|
||||
|
||||
private void setupBeatmapResponse(APIBeatmap b)
|
||||
=> AddStep("setup response", () =>
|
||||
{
|
||||
dummyAPI.HandleRequest = request =>
|
||||
{
|
||||
if (request is GetBeatmapRequest getBeatmapRequest)
|
||||
{
|
||||
getBeatmapRequest.TriggerSuccess(b);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
});
|
||||
|
||||
private void setupFailedResponse()
|
||||
=> AddStep("setup failed response", () =>
|
||||
{
|
||||
dummyAPI.HandleRequest = request =>
|
||||
{
|
||||
request.TriggerFailure(new WebException());
|
||||
return true;
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
@ -110,5 +110,31 @@ namespace osu.Game.Tests.Visual.Online
|
||||
}
|
||||
}, new OsuRuleset().RulesetInfo));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPreviousUsernames()
|
||||
{
|
||||
AddStep("Show user w/ previous usernames", () => header.User.Value = new UserProfileData(new APIUser
|
||||
{
|
||||
Id = 727,
|
||||
Username = "SomeoneIndecisive",
|
||||
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg",
|
||||
Groups = new[]
|
||||
{
|
||||
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
|
||||
},
|
||||
Statistics = new UserStatistics
|
||||
{
|
||||
IsRanked = false,
|
||||
// web will sometimes return non-empty rank history even for unranked users.
|
||||
RankHistory = new APIRankHistory
|
||||
{
|
||||
Mode = @"osu",
|
||||
Data = Enumerable.Range(2345, 85).ToArray()
|
||||
},
|
||||
},
|
||||
PreviousUsernames = new[] { "tsrk.", "quoicoubeh", "apagnan", "epita" }
|
||||
}, new OsuRuleset().RulesetInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -134,14 +134,16 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569),
|
||||
Description = "Outstanding help by being a voluntary test subject.",
|
||||
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor.jpg",
|
||||
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor-new@2x.png",
|
||||
ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor-new.png",
|
||||
Url = "https://osu.ppy.sh/wiki/en/People/Community_Contributors",
|
||||
},
|
||||
new Badge
|
||||
{
|
||||
AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569),
|
||||
Description = "Badge without a url.",
|
||||
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor.jpg",
|
||||
ImageUrl = "https://assets.ppy.sh/profile-badges/contributor@2x.png",
|
||||
ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor.png",
|
||||
},
|
||||
},
|
||||
Title = "osu!volunteer",
|
||||
|
@ -5,20 +5,29 @@ using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Profile.Header.Components;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneUserProfilePreviousUsernames : OsuTestScene
|
||||
public partial class TestSceneUserProfilePreviousUsernamesDisplay : OsuTestScene
|
||||
{
|
||||
private PreviousUsernames container = null!;
|
||||
private PreviousUsernamesDisplay container = null!;
|
||||
private OverlayColourProvider colourProvider = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
Child = container = new PreviousUsernames
|
||||
colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
|
||||
Child = new DependencyProvidingContainer
|
||||
{
|
||||
Child = container = new PreviousUsernamesDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
CachedDependencies = new (Type, object)[] { (typeof(OverlayColourProvider), colourProvider) },
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
@ -515,6 +515,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
checkVisibleItemCount(false, local_set_count * local_diff_count);
|
||||
|
||||
var firstAdded = TestResources.CreateTestBeatmapSetInfo(local_diff_count);
|
||||
firstAdded.Status = BeatmapOnlineStatus.Loved;
|
||||
|
||||
AddStep("Add new set", () => carousel.UpdateBeatmapSet(firstAdded));
|
||||
|
||||
@ -1176,12 +1177,18 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
if (beatmapSets == null)
|
||||
{
|
||||
beatmapSets = new List<BeatmapSetInfo>();
|
||||
var statuses = Enum.GetValues<BeatmapOnlineStatus>()
|
||||
.Except(new[] { BeatmapOnlineStatus.None }) // make sure a badge is always shown.
|
||||
.ToArray();
|
||||
|
||||
for (int i = 1; i <= (setCount ?? set_count); i++)
|
||||
{
|
||||
beatmapSets.Add(randomDifficulties
|
||||
var set = randomDifficulties
|
||||
? TestResources.CreateTestBeatmapSetInfo()
|
||||
: TestResources.CreateTestBeatmapSetInfo(diffCount ?? diff_count));
|
||||
: TestResources.CreateTestBeatmapSetInfo(diffCount ?? diff_count);
|
||||
set.Status = statuses[RNG.Next(statuses.Length)];
|
||||
|
||||
beatmapSets.Add(set);
|
||||
}
|
||||
}
|
||||
|
||||
|
219
osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs
Normal file
219
osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedgeV2.cs
Normal file
@ -0,0 +1,219 @@
|
||||
// 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.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Screens.Select;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneBeatmapInfoWedgeV2 : OsuTestScene
|
||||
{
|
||||
private RulesetStore rulesets = null!;
|
||||
private TestBeatmapInfoWedgeV2 infoWedge = null!;
|
||||
private readonly List<IBeatmap> beatmaps = new List<IBeatmap>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RulesetStore rulesets)
|
||||
{
|
||||
this.rulesets = rulesets;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
// This exists only to make the wedge more visible in the test scene
|
||||
new Box
|
||||
{
|
||||
Y = -20,
|
||||
Colour = Colour4.Cornsilk.Darken(0.2f),
|
||||
Height = BeatmapInfoWedgeV2.WEDGE_HEIGHT + 40,
|
||||
Width = 0.65f,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Margin = new MarginPadding { Top = 20, Left = -10 }
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Top = 20 },
|
||||
Child = infoWedge = new TestBeatmapInfoWedgeV2
|
||||
{
|
||||
Width = 0.6f,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
AddSliderStep("change star difficulty", 0, 11.9, 5.55, v =>
|
||||
{
|
||||
foreach (var hasCurrentValue in infoWedge.ChildrenOfType<IHasCurrentValue<StarDifficulty>>())
|
||||
hasCurrentValue.Current.Value = new StarDifficulty(v, 0);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRulesetChange()
|
||||
{
|
||||
selectBeatmap(Beatmap.Value.Beatmap);
|
||||
|
||||
AddWaitStep("wait for select", 3);
|
||||
|
||||
foreach (var rulesetInfo in rulesets.AvailableRulesets)
|
||||
{
|
||||
var instance = rulesetInfo.CreateInstance();
|
||||
var testBeatmap = createTestBeatmap(rulesetInfo);
|
||||
|
||||
beatmaps.Add(testBeatmap);
|
||||
|
||||
setRuleset(rulesetInfo);
|
||||
|
||||
selectBeatmap(testBeatmap);
|
||||
|
||||
testBeatmapLabels(instance);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWedgeVisibility()
|
||||
{
|
||||
AddStep("hide", () => { infoWedge.Hide(); });
|
||||
AddWaitStep("wait for hide", 3);
|
||||
AddAssert("check visibility", () => infoWedge.Alpha == 0);
|
||||
AddStep("show", () => { infoWedge.Show(); });
|
||||
AddWaitStep("wait for show", 1);
|
||||
AddAssert("check visibility", () => infoWedge.Alpha > 0);
|
||||
}
|
||||
|
||||
private void testBeatmapLabels(Ruleset ruleset)
|
||||
{
|
||||
AddAssert("check title", () => infoWedge.Info!.TitleLabel.Current.Value == $"{ruleset.ShortName}Title");
|
||||
AddAssert("check artist", () => infoWedge.Info!.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist");
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("reset mods", () => SelectedMods.SetDefault());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTruncation()
|
||||
{
|
||||
selectBeatmap(createLongMetadata());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNullBeatmapWithBackground()
|
||||
{
|
||||
selectBeatmap(null);
|
||||
AddAssert("check default title", () => infoWedge.Info!.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title);
|
||||
AddAssert("check default artist", () => infoWedge.Info!.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist);
|
||||
AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
|
||||
}
|
||||
|
||||
private void setRuleset(RulesetInfo rulesetInfo)
|
||||
{
|
||||
Container? containerBefore = null;
|
||||
|
||||
AddStep("set ruleset", () =>
|
||||
{
|
||||
// wedge content is only refreshed if the ruleset changes, so only wait for load in that case.
|
||||
if (!rulesetInfo.Equals(Ruleset.Value))
|
||||
containerBefore = infoWedge.DisplayedContent;
|
||||
|
||||
Ruleset.Value = rulesetInfo;
|
||||
});
|
||||
|
||||
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
|
||||
}
|
||||
|
||||
private void selectBeatmap(IBeatmap? b)
|
||||
{
|
||||
Container? containerBefore = null;
|
||||
|
||||
AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
|
||||
{
|
||||
containerBefore = infoWedge.DisplayedContent;
|
||||
infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b);
|
||||
infoWedge.Show();
|
||||
});
|
||||
|
||||
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
|
||||
}
|
||||
|
||||
private IBeatmap createTestBeatmap(RulesetInfo ruleset)
|
||||
{
|
||||
List<HitObject> objects = new List<HitObject>();
|
||||
for (double i = 0; i < 50000; i += 1000)
|
||||
objects.Add(new TestHitObject { StartTime = i });
|
||||
|
||||
return new Beatmap
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Author = { Username = $"{ruleset.ShortName}Author" },
|
||||
Artist = $"{ruleset.ShortName}Artist",
|
||||
Source = $"{ruleset.ShortName}Source",
|
||||
Title = $"{ruleset.ShortName}Title"
|
||||
},
|
||||
Ruleset = ruleset,
|
||||
StarRating = 6,
|
||||
DifficultyName = $"{ruleset.ShortName}Version",
|
||||
Difficulty = new BeatmapDifficulty()
|
||||
},
|
||||
HitObjects = objects
|
||||
};
|
||||
}
|
||||
|
||||
private IBeatmap createLongMetadata()
|
||||
{
|
||||
return new Beatmap
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Author = { Username = "WWWWWWWWWWWWWWW" },
|
||||
Artist = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Artist",
|
||||
Source = "Verrrrry long Source",
|
||||
Title = "Verrrrry long Title"
|
||||
},
|
||||
DifficultyName = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Version",
|
||||
Status = BeatmapOnlineStatus.Graveyard,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private partial class TestBeatmapInfoWedgeV2 : BeatmapInfoWedgeV2
|
||||
{
|
||||
public new Container? DisplayedContent => base.DisplayedContent;
|
||||
public new WedgeInfoText? Info => base.Info;
|
||||
}
|
||||
|
||||
private class TestHitObject : ConvertHitObject, IHasPosition
|
||||
{
|
||||
public float X => 0;
|
||||
public float Y => 0;
|
||||
public Vector2 Position { get; } = Vector2.Zero;
|
||||
}
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets;
|
||||
@ -1087,6 +1088,25 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddAssert("0 matching shown", () => songSelect.ChildrenOfType<FilterControl>().Single().InformationalText == "0 matches");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDeleteHotkey()
|
||||
{
|
||||
createSongSelect();
|
||||
|
||||
addRulesetImportStep(0);
|
||||
AddAssert("3 matching shown", () => songSelect.ChildrenOfType<FilterControl>().Single().InformationalText == "3 matches");
|
||||
|
||||
AddStep("press shift-delete", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ShiftLeft);
|
||||
InputManager.Key(Key.Delete);
|
||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||
});
|
||||
AddUntilStep("delete dialog shown", () => DialogOverlay.CurrentDialog, Is.InstanceOf<BeatmapDeleteDialog>);
|
||||
AddStep("confirm deletion", () => DialogOverlay.CurrentDialog!.PerformAction<PopupDialogDangerousButton>());
|
||||
AddAssert("0 matching shown", () => songSelect.ChildrenOfType<FilterControl>().Single().InformationalText == "0 matches");
|
||||
}
|
||||
|
||||
private void waitForInitialSelection()
|
||||
{
|
||||
AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault);
|
||||
|
@ -6,9 +6,11 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Select.FooterV2;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -37,10 +39,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
footer = new FooterV2
|
||||
new PopoverContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = footer = new FooterV2(),
|
||||
},
|
||||
overlay = new DummyOverlay()
|
||||
};
|
||||
@ -56,6 +58,24 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
overlay.Hide();
|
||||
});
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShowOptions()
|
||||
{
|
||||
AddStep("enable options", () =>
|
||||
{
|
||||
var optionsButton = this.ChildrenOfType<FooterButtonV2>().Last();
|
||||
|
||||
optionsButton.Enabled.Value = true;
|
||||
optionsButton.TriggerClick();
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestState()
|
||||
{
|
||||
|
@ -0,0 +1,34 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Tests.Scores.IO;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneMissingBeatmapNotification : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Child = new Container
|
||||
{
|
||||
Width = 280,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Child = new MissingBeatmapNotification(CreateAPIBeatmapSet(Ruleset.Value).Beatmaps.First(), new ImportScoreTest.TestArchiveReader(), "deadbeef")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneModEffectPreviewPanel : OsuTestScene
|
||||
{
|
||||
[Cached(typeof(BeatmapDifficultyCache))]
|
||||
private TestBeatmapDifficultyCache difficultyCache = new TestBeatmapDifficultyCache();
|
||||
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
private Container content = null!;
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
private BeatmapAttributesDisplay panel = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
base.Content.AddRange(new Drawable[]
|
||||
{
|
||||
difficultyCache,
|
||||
content = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDisplay()
|
||||
{
|
||||
OsuModDifficultyAdjust difficultyAdjust = new OsuModDifficultyAdjust();
|
||||
OsuModDoubleTime doubleTime = new OsuModDoubleTime();
|
||||
|
||||
AddStep("create display", () => Child = panel = new BeatmapAttributesDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
|
||||
AddStep("set beatmap", () =>
|
||||
{
|
||||
var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo)
|
||||
{
|
||||
BeatmapInfo =
|
||||
{
|
||||
BPM = 120
|
||||
}
|
||||
};
|
||||
|
||||
Ruleset.Value = beatmap.BeatmapInfo.Ruleset;
|
||||
panel.BeatmapInfo.Value = beatmap.BeatmapInfo;
|
||||
});
|
||||
|
||||
AddSliderStep("change star rating", 0, 10d, 5, stars =>
|
||||
{
|
||||
if (panel.IsNotNull())
|
||||
previewStarRating(stars);
|
||||
});
|
||||
AddStep("preview ridiculously high SR", () => previewStarRating(1234));
|
||||
|
||||
AddStep("add DA to mods", () => SelectedMods.Value = new[] { difficultyAdjust });
|
||||
|
||||
AddSliderStep("change AR", 0, 10f, 5, ar =>
|
||||
{
|
||||
if (panel.IsNotNull())
|
||||
difficultyAdjust.ApproachRate.Value = ar;
|
||||
});
|
||||
AddSliderStep("change CS", 0, 10f, 5, cs =>
|
||||
{
|
||||
if (panel.IsNotNull())
|
||||
difficultyAdjust.CircleSize.Value = cs;
|
||||
});
|
||||
AddSliderStep("change HP", 0, 10f, 5, hp =>
|
||||
{
|
||||
if (panel.IsNotNull())
|
||||
difficultyAdjust.DrainRate.Value = hp;
|
||||
});
|
||||
AddSliderStep("change OD", 0, 10f, 5, od =>
|
||||
{
|
||||
if (panel.IsNotNull())
|
||||
difficultyAdjust.OverallDifficulty.Value = od;
|
||||
});
|
||||
|
||||
AddStep("add DT to mods", () => SelectedMods.Value = new Mod[] { difficultyAdjust, doubleTime });
|
||||
AddSliderStep("change rate", 1.01d, 2d, 1.5d, rate =>
|
||||
{
|
||||
if (panel.IsNotNull())
|
||||
doubleTime.SpeedChange.Value = rate;
|
||||
});
|
||||
|
||||
AddToggleStep("toggle collapsed", collapsed => panel.Collapsed.Value = collapsed);
|
||||
}
|
||||
|
||||
private void previewStarRating(double stars)
|
||||
{
|
||||
difficultyCache.Difficulty = new StarDifficulty(stars, 0);
|
||||
panel.BeatmapInfo.TriggerChange();
|
||||
}
|
||||
|
||||
private partial class TestBeatmapDifficultyCache : BeatmapDifficultyCache
|
||||
{
|
||||
public StarDifficulty? Difficulty { get; set; }
|
||||
|
||||
public override Task<StarDifficulty?> GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, IEnumerable<Mod>? mods = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(Difficulty);
|
||||
}
|
||||
}
|
||||
}
|
@ -51,6 +51,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddStep("clear contents", Clear);
|
||||
AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0));
|
||||
AddStep("reset mods", () => SelectedMods.SetDefault());
|
||||
AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo));
|
||||
AddStep("set up presets", () =>
|
||||
{
|
||||
Realm.Write(r =>
|
||||
@ -92,6 +93,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
State = { Value = Visibility.Visible },
|
||||
Beatmap = Beatmap.Value,
|
||||
SelectedMods = { BindTarget = SelectedMods }
|
||||
});
|
||||
waitForColumnLoad();
|
||||
@ -113,7 +115,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("mod multiplier correct", () =>
|
||||
{
|
||||
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
|
||||
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value);
|
||||
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value);
|
||||
});
|
||||
assertCustomisationToggleState(disabled: false, active: false);
|
||||
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
|
||||
@ -128,7 +130,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("mod multiplier correct", () =>
|
||||
{
|
||||
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
|
||||
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value);
|
||||
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value);
|
||||
});
|
||||
assertCustomisationToggleState(disabled: false, active: false);
|
||||
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
|
||||
@ -785,7 +787,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<ModPresetPanel>().Single(preset => preset.Preset.Value.Name == "Half Time 0.5x"));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddAssert("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.5));
|
||||
AddAssert("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.5));
|
||||
|
||||
// this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation,
|
||||
// it is instrumental in the reproduction of the failure scenario that this test is supposed to cover.
|
||||
@ -794,7 +796,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick());
|
||||
AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType<ModSettingsArea>().Single()
|
||||
.ChildrenOfType<RevertToDefaultButton<double>>().Single().TriggerClick());
|
||||
AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.7));
|
||||
AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.7));
|
||||
}
|
||||
|
||||
private void waitForColumnLoad() => AddUntilStep("all column content loaded",
|
||||
|
@ -1,68 +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.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneModsEffectDisplay : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Test]
|
||||
public void TestModsEffectDisplay()
|
||||
{
|
||||
TestDisplay testDisplay = null!;
|
||||
Box background = null!;
|
||||
|
||||
AddStep("add display", () =>
|
||||
{
|
||||
Add(testDisplay = new TestDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
});
|
||||
var boxes = testDisplay.ChildrenOfType<Box>();
|
||||
background = boxes.First();
|
||||
});
|
||||
|
||||
AddStep("set value to default", () => testDisplay.Current.Value = 50);
|
||||
AddUntilStep("colours are correct", () => testDisplay.Container.Colour == Color4.White && background.Colour == colourProvider.Background3);
|
||||
|
||||
AddStep("set value to less", () => testDisplay.Current.Value = 40);
|
||||
AddUntilStep("colours are correct", () => testDisplay.Container.Colour == colourProvider.Background5 && background.Colour == colours.ForModType(ModType.DifficultyReduction));
|
||||
|
||||
AddStep("set value to bigger", () => testDisplay.Current.Value = 60);
|
||||
AddUntilStep("colours are correct", () => testDisplay.Container.Colour == colourProvider.Background5 && background.Colour == colours.ForModType(ModType.DifficultyIncrease));
|
||||
}
|
||||
|
||||
private partial class TestDisplay : ModsEffectDisplay
|
||||
{
|
||||
public Container<Drawable> Container => Content;
|
||||
|
||||
protected override LocalisableString Label => "Test display";
|
||||
|
||||
public TestDisplay()
|
||||
{
|
||||
Current.Default = 50;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
@ -12,17 +11,17 @@ using osu.Game.Overlays.Mods;
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneDifficultyMultiplierDisplay : OsuTestScene
|
||||
public partial class TestSceneScoreMultiplierDisplay : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
|
||||
|
||||
[Test]
|
||||
public void TestDifficultyMultiplierDisplay()
|
||||
public void TestBasic()
|
||||
{
|
||||
DifficultyMultiplierDisplay multiplierDisplay = null;
|
||||
ScoreMultiplierDisplay multiplierDisplay = null!;
|
||||
|
||||
AddStep("create content", () => Child = multiplierDisplay = new DifficultyMultiplierDisplay
|
||||
AddStep("create content", () => Child = multiplierDisplay = new ScoreMultiplierDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
@ -34,7 +33,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
|
||||
AddSliderStep("set multiplier", 0, 2, 1d, multiplier =>
|
||||
{
|
||||
if (multiplierDisplay != null)
|
||||
if (multiplierDisplay.IsNotNull())
|
||||
multiplierDisplay.Current.Value = multiplier;
|
||||
});
|
||||
}
|
@ -2,10 +2,10 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="DeepEqual" Version="4.2.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
|
@ -4,9 +4,9 @@
|
||||
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
@ -126,7 +126,7 @@ namespace osu.Game.Tournament.Screens.MapPool
|
||||
if (CurrentMatch.Value == null || CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) < 2)
|
||||
return;
|
||||
|
||||
// if bans have already been placed, beatmap changes result in a selection being made autoamtically
|
||||
// if bans have already been placed, beatmap changes result in a selection being made automatically
|
||||
if (beatmap.NewValue?.OnlineID > 0)
|
||||
addForBeatmap(beatmap.NewValue.OnlineID);
|
||||
}
|
||||
|
@ -319,7 +319,7 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
DateTimeOffset dateAdded = DateTimeOffset.UtcNow;
|
||||
|
||||
if (reader is LegacyDirectoryArchiveReader legacyReader)
|
||||
if (reader is DirectoryArchiveReader legacyReader)
|
||||
{
|
||||
var beatmaps = reader.Filenames.Where(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user