1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-21 13:06:53 +08:00

Compare commits

...

1050 Commits

805 changed files with 15613 additions and 5635 deletions
+2 -2
View File
@@ -15,7 +15,7 @@
]
},
"smoogipoo.nvika": {
"version": "1.0.1",
"version": "1.0.3",
"commands": [
"nvika"
]
@@ -33,4 +33,4 @@
]
}
}
}
}
+1 -1
View File
@@ -113,7 +113,7 @@ dotnet_style_qualification_for_event = false:warning
dotnet_style_predefined_type_for_locals_parameters_members = true:warning
dotnet_style_predefined_type_for_member_access = true:warning
csharp_style_var_when_type_is_apparent = true:none
csharp_style_var_for_built_in_types = true:none
csharp_style_var_for_built_in_types = false:warning
csharp_style_var_elsewhere = true:silent
#Style - modifiers
+1
View File
@@ -1 +1,2 @@
github: ppy
custom: https://osu.ppy.sh/home/support
+50 -3
View File
@@ -50,6 +50,48 @@ jobs:
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx
build-only-android:
name: Build only (Android)
runs-on: windows-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install .NET 5.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "5.0.x"
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v1
- name: Build
run: msbuild osu.Android.slnf /restore /p:Configuration=Debug
build-only-ios:
# While this workflow technically *can* run, it fails as iOS builds are blocked by multiple issues.
# See https://github.com/ppy/osu-framework/issues/4677 for the details.
# The job can be unblocked once those issues are resolved and game deployments can happen again.
if: false
name: Build only (iOS)
runs-on: macos-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install .NET 5.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "5.0.x"
# Contrary to seemingly any other msbuild, msbuild running on macOS/Mono
# cannot accept .sln(f) files as arguments.
# Build just the main game for now.
- name: Build
run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug
inspect-code:
name: Code Quality
runs-on: ubuntu-latest
@@ -79,9 +121,14 @@ jobs:
run: |
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
# FIXME: Suppress warnings from templates project
dotnet codefilesanity | while read -r line; do
echo "::warning::$line"
done
exit_code=0
while read -r line; do
if [[ ! -z "$line" ]]; then
echo "::error::$line"
exit_code=1
fi
done <<< $(dotnet codefilesanity)
exit $exit_code
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
# - name: .NET Format (Dry Run)
+2
View File
@@ -30,3 +30,5 @@ jobs:
name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
path: "*.trx"
reporter: dotnet-trx
list-suites: 'failed'
list-tests: 'failed'
+1 -1
View File
@@ -16,7 +16,7 @@
<EmbeddedResource Include="Resources\**\*.*" />
</ItemGroup>
<ItemGroup Label="Code Analysis">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.2" PrivateAssets="All" />
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3" PrivateAssets="All" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3" PrivateAssets="All" />
</ItemGroup>
@@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
@@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
@@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
@@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
+2 -2
View File
@@ -51,8 +51,8 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1004.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1026.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1103.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
+1
View File
@@ -20,6 +20,7 @@ namespace osu.Android
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-beatmap-archive")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-skin-archive")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-replay")]
+2 -2
View File
@@ -156,7 +156,7 @@ namespace osu.Desktop
{
lock (importableFiles)
{
var firstExtension = Path.GetExtension(filePaths.First());
string firstExtension = Path.GetExtension(filePaths.First());
if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return;
@@ -177,7 +177,7 @@ namespace osu.Desktop
{
Logger.Log($"Handling batch import of {importableFiles.Count} files");
var paths = importableFiles.ToArray();
string[] paths = importableFiles.ToArray();
importableFiles.Clear();
Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning);
+6 -6
View File
@@ -22,17 +22,17 @@ namespace osu.Desktop
public static int Main(string[] args)
{
// Back up the cwd before DesktopGameHost changes it
var cwd = Environment.CurrentDirectory;
string cwd = Environment.CurrentDirectory;
string gameName = base_game_name;
bool tournamentClient = false;
foreach (var arg in args)
foreach (string arg in args)
{
var split = arg.Split('=');
string[] split = arg.Split('=');
var key = split[0];
var val = split.Length > 1 ? split[1] : string.Empty;
string key = split[0];
string val = split.Length > 1 ? split[1] : string.Empty;
switch (key)
{
@@ -62,7 +62,7 @@ namespace osu.Desktop
{
var importer = new ArchiveImportIPCChannel(host);
foreach (var file in args)
foreach (string file in args)
{
Console.WriteLine(@"Importing {0}", file);
if (!importer.ImportAsync(Path.GetFullPath(file, cwd)).Wait(3000))
@@ -14,11 +14,11 @@ namespace osu.Game.Rulesets.Catch.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
[TestCase(4.050601681491468d, "diffcalc-test")]
[TestCase(4.0505463516206195d, "diffcalc-test")]
public void Test(double expected, string name)
=> base.Test(expected, name);
[TestCase(5.169743871843191d, "diffcalc-test")]
[TestCase(5.1696411260785498d, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new CatchModDoubleTime());
@@ -29,8 +29,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
protected CatchSelectionBlueprintTestScene()
{
EditorBeatmap = new EditorBeatmap(new CatchBeatmap());
EditorBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = 0;
EditorBeatmap = new EditorBeatmap(new CatchBeatmap()) { Difficulty = { CircleSize = 0 } };
EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint
{
BeatLength = 100
@@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
@@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
protected override void AddHitObject(DrawableHitObject hitObject)
{
// Create nested bananas (but positions are not randomized because beatmap processing is not done).
hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), Beatmap.Value.BeatmapInfo.BaseDifficulty);
hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
base.AddHitObject(hitObject);
}
@@ -0,0 +1,91 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class TestSceneCatchDistanceSnapGrid : OsuManualInputManagerTestScene
{
private readonly ManualClock manualClock = new ManualClock();
[Cached(typeof(Playfield))]
private readonly CatchPlayfield playfield;
private ScrollingHitObjectContainer hitObjectContainer => playfield.HitObjectContainer;
private readonly CatchDistanceSnapGrid distanceGrid;
private readonly FruitOutline fruitOutline;
private readonly Fruit fruit = new Fruit();
public TestSceneCatchDistanceSnapGrid()
{
Child = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Width = 500,
Children = new Drawable[]
{
new ScrollingTestContainer(ScrollingDirection.Down)
{
RelativeSizeAxes = Axes.Both,
Child = playfield = new CatchPlayfield(new BeatmapDifficulty())
{
RelativeSizeAxes = Axes.Both,
Clock = new FramedClock(manualClock)
}
},
distanceGrid = new CatchDistanceSnapGrid(new double[] { 0, -1, 1 }),
fruitOutline = new FruitOutline()
},
};
}
protected override void Update()
{
base.Update();
distanceGrid.StartTime = 100;
distanceGrid.StartX = 250;
Vector2 screenSpacePosition = InputManager.CurrentState.Mouse.Position;
var result = distanceGrid.GetSnappedPosition(screenSpacePosition);
if (result != null)
{
fruit.OriginalX = hitObjectContainer.ToLocalSpace(result.ScreenSpacePosition).X;
if (result.Time != null)
fruit.StartTime = result.Time.Value;
}
fruitOutline.Position = CatchHitObjectUtils.GetStartPosition(hitObjectContainer, fruit);
fruitOutline.UpdateFrom(fruit);
}
protected override bool OnScroll(ScrollEvent e)
{
manualClock.CurrentTime -= e.ScrollDelta.Y * 50;
return true;
}
}
}
@@ -4,9 +4,9 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
@@ -23,11 +23,12 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
[BackgroundDependencyLoader]
private void load()
protected override IBeatmap GetPlayableBeatmap()
{
Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderTickRate = 5;
Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity * 10;
var playable = base.GetPlayableBeatmap();
playable.Difficulty.SliderTickRate = 5;
playable.Difficulty.SliderMultiplier = velocity * 10;
return playable;
}
[Test]
@@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddAssert("correct outline count", () =>
{
var expected = hitObject.NestedHitObjects.Count(h => !(h is TinyDroplet));
int expected = hitObject.NestedHitObjects.Count(h => !(h is TinyDroplet));
return this.ChildrenOfType<FruitOutline>().Count() == expected;
});
AddAssert("correct vertex piece count", () =>
@@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
X = x,
Path = sliderPath,
};
EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity;
EditorBeatmap.Difficulty.SliderMultiplier = velocity;
EditorBeatmap.Add(hitObject);
EditorBeatmap.Update(hitObject);
Assert.That(hitObject.Velocity, Is.EqualTo(velocity));
@@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[Test]
public void TestCatcherCatchWidth()
{
var halfWidth = Catcher.CalculateCatchWidth(new BeatmapDifficulty { CircleSize = 0 }) / 2;
float halfWidth = Catcher.CalculateCatchWidth(new BeatmapDifficulty { CircleSize = 0 }) / 2;
AddStep("catch fruit", () =>
{
attemptCatch(new Fruit { X = -halfWidth + 1 });
@@ -237,7 +237,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private void attemptCatch(Func<CatchHitObject> hitObject, int count)
{
for (var i = 0; i < count; i++)
for (int i = 0; i < count; i++)
attemptCatch(hitObject(), out _, out _);
}
@@ -290,7 +290,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public IEnumerable<CaughtObject> CaughtObjects => this.ChildrenOfType<CaughtObject>();
public TestCatcher(DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty)
public TestCatcher(DroppedObjectContainer droppedObjectTarget, IBeatmapDifficultyInfo difficulty)
: base(droppedObjectTarget, difficulty)
{
}
@@ -298,7 +298,7 @@ namespace osu.Game.Rulesets.Catch.Tests
public class TestKiaiFruit : Fruit
{
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private ScheduledDelegate addManyFruit;
private BeatmapDifficulty beatmapDifficulty;
private IBeatmapDifficultyInfo beatmapDifficulty;
public TestSceneCatcherArea()
{
@@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private class TestCatcherArea : CatcherArea
{
public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
public TestCatcherArea(IBeatmapDifficultyInfo beatmapDifficulty)
{
var droppedObjectContainer = new DroppedObjectContainer();
Add(droppedObjectContainer);
@@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private void spawnJuiceStream(bool hit = false)
{
var xCoords = getXCoords(hit);
float xCoords = getXCoords(hit);
var juice = new JuiceStream
{
@@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
@@ -211,7 +211,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2;
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) / 2;
// Todo: This is wrong. osu!stable calculated hyperdashes using the full catcher size, excluding the margins.
// This should theoretically cause impossible scenarios, but practically, likely due to the size of the playfield, it doesn't seem possible.
@@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
int thisDirection = nextObject.EffectiveX > currentObject.EffectiveX ? 1 : -1;
double timeToNext = nextObject.StartTime - currentObject.StartTime - 1000f / 60f / 4; // 1/4th of a frame of grace time, taken from osu-stable
double distanceToNext = Math.Abs(nextObject.EffectiveX - currentObject.EffectiveX) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth);
float distanceToHyper = (float)(timeToNext * Catcher.BASE_SPEED - distanceToNext);
float distanceToHyper = (float)(timeToNext * Catcher.BASE_DASH_SPEED - distanceToNext);
if (distanceToHyper < 0)
{
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
return new CatchDifficultyAttributes { Mods = mods, Skills = skills };
// this is the same as osu!, so there's potential to share the implementation... maybe
double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
return new CatchDifficultyAttributes
{
@@ -69,10 +69,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
{
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) * 0.5f;
// For circle sizes above 5.5, reduce the catcher width further to simulate imperfect gameplay.
halfCatcherWidth *= 1 - (Math.Max(0, beatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5.5f) * 0.0625f);
halfCatcherWidth *= 1 - (Math.Max(0, beatmap.Difficulty.CircleSize - 5.5f) * 0.0625f);
return new Skill[]
{
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
: base(hitObject, lastObject, clockRate)
{
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
var scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
float scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
NormalizedPosition = BaseObject.EffectiveX * scalingFactor;
LastNormalizedPosition = LastObject.EffectiveX * scalingFactor;
@@ -0,0 +1,141 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit
{
/// <summary>
/// The guide lines used in the osu!catch editor to compose patterns that can be caught with constant speed.
/// Currently, only forward placement (an object is snapped based on the previous object, not the opposite) is supported.
/// </summary>
public class CatchDistanceSnapGrid : CompositeDrawable
{
public double StartTime { get; set; }
public float StartX { get; set; }
private const double max_vertical_line_length_in_time = CatchPlayfield.WIDTH / Catcher.BASE_WALK_SPEED;
private readonly double[] velocities;
private readonly List<Path> verticalPaths = new List<Path>();
private readonly List<Vector2[]> verticalLineVertices = new List<Vector2[]>();
[Resolved]
private Playfield playfield { get; set; }
private ScrollingHitObjectContainer hitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
public CatchDistanceSnapGrid(double[] velocities)
{
RelativeSizeAxes = Axes.Both;
Anchor = Anchor.BottomLeft;
this.velocities = velocities;
for (int i = 0; i < velocities.Length; i++)
{
verticalPaths.Add(new SmoothPath
{
PathRadius = 2,
Alpha = 0.5f,
});
verticalLineVertices.Add(new[] { Vector2.Zero, Vector2.Zero });
}
AddRangeInternal(verticalPaths);
}
protected override void Update()
{
base.Update();
double currentTime = hitObjectContainer.Time.Current;
for (int i = 0; i < velocities.Length; i++)
{
double velocity = velocities[i];
// The line ends at the top of the playfield.
double endTime = hitObjectContainer.TimeAtPosition(-hitObjectContainer.DrawHeight, currentTime);
// Non-vertical lines are cut at the sides of the playfield.
// Vertical lines are cut at some reasonable length.
if (velocity > 0)
endTime = Math.Min(endTime, StartTime + (CatchPlayfield.WIDTH - StartX) / velocity);
else if (velocity < 0)
endTime = Math.Min(endTime, StartTime + StartX / -velocity);
else
endTime = Math.Min(endTime, StartTime + max_vertical_line_length_in_time);
Vector2[] lineVertices = verticalLineVertices[i];
lineVertices[0] = calculatePosition(velocity, StartTime);
lineVertices[1] = calculatePosition(velocity, endTime);
var verticalPath = verticalPaths[i];
verticalPath.Vertices = verticalLineVertices[i];
verticalPath.OriginPosition = verticalPath.PositionInBoundingBox(Vector2.Zero);
}
Vector2 calculatePosition(double velocity, double time)
{
// Don't draw inverted lines.
time = Math.Max(time, StartTime);
float x = StartX + (float)((time - StartTime) * velocity);
float y = hitObjectContainer.PositionAtTime(time, currentTime);
return new Vector2(x, y);
}
}
[CanBeNull]
public SnapResult GetSnappedPosition(Vector2 screenSpacePosition)
{
double time = hitObjectContainer.TimeAtScreenSpacePosition(screenSpacePosition);
// If the cursor is below the distance snap grid, snap to the origin.
// Not returning `null` to retain the continuous snapping behavior when the cursor is slightly below the origin.
// This behavior is not currently visible in the editor because editor chooses the snap start time based on the mouse position.
if (time <= StartTime)
{
float y = hitObjectContainer.PositionAtTime(StartTime);
Vector2 originPosition = hitObjectContainer.ToScreenSpace(new Vector2(StartX, y));
return new SnapResult(originPosition, StartTime);
}
return enumerateSnappingCandidates(time)
.OrderBy(pos => Vector2.DistanceSquared(screenSpacePosition, pos.ScreenSpacePosition))
.FirstOrDefault();
}
private IEnumerable<SnapResult> enumerateSnappingCandidates(double time)
{
float y = hitObjectContainer.PositionAtTime(time);
foreach (double velocity in velocities)
{
float x = (float)(StartX + (time - StartTime) * velocity);
Vector2 screenSpacePosition = hitObjectContainer.ToScreenSpace(new Vector2(x, y + hitObjectContainer.DrawHeight));
yield return new SnapResult(screenSpacePosition, time);
}
}
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
}
}
@@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Catch.Edit
public class CatchEditorPlayfield : CatchPlayfield
{
// TODO fixme: the size of the catcher is not changed when circle size is changed in setup screen.
public CatchEditorPlayfield(BeatmapDifficulty difficulty)
public CatchEditorPlayfield(IBeatmapDifficultyInfo difficulty)
: base(difficulty)
{
}
@@ -2,14 +2,23 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@@ -17,6 +26,14 @@ namespace osu.Game.Rulesets.Catch.Edit
{
public class CatchHitObjectComposer : HitObjectComposer<CatchHitObject>
{
private const float distance_snap_radius = 50;
private CatchDistanceSnapGrid distanceSnapGrid;
private readonly Bindable<TernaryState> distanceSnapToggle = new Bindable<TernaryState>();
private InputManager inputManager;
public CatchHitObjectComposer(CatchRuleset ruleset)
: base(ruleset)
{
@@ -30,6 +47,27 @@ namespace osu.Game.Rulesets.Catch.Edit
RelativeSizeAxes = Axes.Both,
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
});
LayerBelowRuleset.Add(distanceSnapGrid = new CatchDistanceSnapGrid(new[]
{
0.0,
Catcher.BASE_DASH_SPEED, -Catcher.BASE_DASH_SPEED,
Catcher.BASE_WALK_SPEED, -Catcher.BASE_WALK_SPEED,
}));
}
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
}
protected override void Update()
{
base.Update();
updateDistanceSnapGrid();
}
protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) =>
@@ -42,14 +80,95 @@ namespace osu.Game.Rulesets.Catch.Edit
new BananaShowerCompositionTool()
};
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
{
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
});
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
{
var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
// TODO: implement position snap
result.ScreenSpacePosition.X = screenSpacePosition.X;
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
{
result = snapResult;
}
return result;
}
protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this);
[CanBeNull]
private PalpableCatchHitObject getLastSnappableHitObject(double time)
{
var hitObject = EditorBeatmap.HitObjects.OfType<CatchHitObject>().LastOrDefault(h => h.GetEndTime() < time && !(h is BananaShower));
switch (hitObject)
{
case Fruit fruit:
return fruit;
case JuiceStream juiceStream:
return juiceStream.NestedHitObjects.OfType<PalpableCatchHitObject>().LastOrDefault(h => !(h is TinyDroplet));
default:
return null;
}
}
[CanBeNull]
private PalpableCatchHitObject getDistanceSnapGridSourceHitObject()
{
switch (BlueprintContainer.CurrentTool)
{
case SelectTool _:
if (EditorBeatmap.SelectedHitObjects.Count == 0)
return null;
double minTime = EditorBeatmap.SelectedHitObjects.Min(hitObject => hitObject.StartTime);
return getLastSnappableHitObject(minTime);
case FruitCompositionTool _:
case JuiceStreamCompositionTool _:
if (!CursorInPlacementArea)
return null;
if (EditorBeatmap.PlacementObject.Value is JuiceStream)
{
// Juice stream path is not subject to snapping.
return null;
}
double timeAtCursor = ((CatchPlayfield)Playfield).TimeAtScreenSpacePosition(inputManager.CurrentState.Mouse.Position);
return getLastSnappableHitObject(timeAtCursor);
default:
return null;
}
}
private void updateDistanceSnapGrid()
{
if (distanceSnapToggle.Value != TernaryState.True)
{
distanceSnapGrid.Hide();
return;
}
var sourceHitObject = getDistanceSnapGridSourceHitObject();
if (sourceHitObject == null)
{
distanceSnapGrid.Hide();
return;
}
distanceSnapGrid.Show();
distanceSnapGrid.StartTime = sourceHitObject.GetEndTime();
distanceSnapGrid.StartX = sourceHitObject.EffectiveX;
}
}
}
@@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Catch.Edit
{
}
protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.Difficulty);
}
}
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad" } },
Replay = new CatchAutoGenerator(beatmap).Generate(),
};
}
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad" } },
Replay = new CatchAutoGenerator(beatmap).Generate(),
};
}
@@ -52,8 +52,8 @@ namespace osu.Game.Rulesets.Catch.Mods
{
var hitObject = drawable.HitObject;
var offset = hitObject.TimePreempt * fade_out_offset_multiplier;
var duration = offset - hitObject.TimePreempt * fade_out_duration_multiplier;
double offset = hitObject.TimePreempt * fade_out_offset_multiplier;
double duration = offset - hitObject.TimePreempt * fade_out_duration_multiplier;
using (drawable.BeginAbsoluteSequence(hitObject.StartTime - offset))
drawable.FadeOut(duration);
@@ -128,11 +128,11 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary>
public int RandomSeed => (int)StartTime;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2;
}
@@ -37,14 +37,13 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary>
public double SpanDuration => Duration / this.SpanCount();
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate;
@@ -146,7 +145,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public double Distance => Path.Distance;
public List<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>();
public IList<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>();
public double? LegacyLastTickOffset { get; set; }
}
@@ -26,9 +26,6 @@ namespace osu.Game.Rulesets.Catch.Replays
if (Beatmap.HitObjects.Count == 0)
return;
// todo: add support for HT DT
const double dash_speed = Catcher.BASE_SPEED;
const double movement_speed = dash_speed / 2;
float lastPosition = CatchPlayfield.CENTER_X;
double lastTime = 0;
@@ -47,8 +44,8 @@ namespace osu.Game.Rulesets.Catch.Replays
// The case where positionChange > 0 and timeAvailable == 0 results in PositiveInfinity which provides expected beheaviour.
double speedRequired = positionChange == 0 ? 0 : positionChange / timeAvailable;
bool dashRequired = speedRequired > movement_speed;
bool impossibleJump = speedRequired > movement_speed * 2;
bool dashRequired = speedRequired > Catcher.BASE_WALK_SPEED;
bool impossibleJump = speedRequired > Catcher.BASE_DASH_SPEED;
// todo: get correct catcher size, based on difficulty CS.
const float catcher_width_half = Catcher.BASE_SIZE * 0.3f * 0.5f;
@@ -73,7 +70,7 @@ namespace osu.Game.Rulesets.Catch.Replays
else if (dashRequired)
{
// we do a movement in two parts - the dash part then the normal part...
double timeAtNormalSpeed = positionChange / movement_speed;
double timeAtNormalSpeed = positionChange / Catcher.BASE_WALK_SPEED;
double timeWeNeedToSave = timeAtNormalSpeed - timeAvailable;
double timeAtDashSpeed = timeWeNeedToSave / 2;
@@ -86,7 +83,7 @@ namespace osu.Game.Rulesets.Catch.Replays
}
else
{
double timeBefore = positionChange / movement_speed;
double timeBefore = positionChange / Catcher.BASE_WALK_SPEED;
addFrame(h.StartTime - timeBefore, lastPosition);
addFrame(h.StartTime, h.EffectiveX);
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Catch.Replays
public override void CollectPendingInputs(List<IInput> inputs)
{
var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
float position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
inputs.Add(new CatchReplayState
{
@@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return null;
case CatchSkinComponents.Catcher:
var version = GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value ?? 1;
decimal version = GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value ?? 1;
if (version < 2.3m)
{
+2 -2
View File
@@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Catch.UI
internal CatcherArea CatcherArea { get; private set; }
private readonly BeatmapDifficulty difficulty;
private readonly IBeatmapDifficultyInfo difficulty;
public CatchPlayfield(BeatmapDifficulty difficulty)
public CatchPlayfield(IBeatmapDifficultyInfo difficulty)
{
this.difficulty = difficulty;
}
+16 -11
View File
@@ -57,14 +57,19 @@ namespace osu.Game.Rulesets.Catch.UI
public bool CatchFruitOnPlate { get; set; } = true;
/// <summary>
/// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable.
/// The speed of the catcher when the catcher is dashing.
/// </summary>
public const double BASE_SPEED = 1.0;
public const double BASE_DASH_SPEED = 1.0;
/// <summary>
/// The current speed of the catcher.
/// The speed of the catcher when the catcher is not dashing.
/// </summary>
public double Speed => (Dashing ? 1 : 0.5) * BASE_SPEED * hyperDashModifier;
public const double BASE_WALK_SPEED = 0.5;
/// <summary>
/// The current speed of the catcher with the hyper-dash modifier applied.
/// </summary>
public double Speed => (Dashing ? BASE_DASH_SPEED : BASE_WALK_SPEED) * hyperDashModifier;
/// <summary>
/// The amount by which caught fruit should be scaled down to fit on the plate.
@@ -124,7 +129,7 @@ namespace osu.Game.Rulesets.Catch.UI
private readonly DrawablePool<CaughtBanana> caughtBananaPool;
private readonly DrawablePool<CaughtDroplet> caughtDropletPool;
public Catcher([NotNull] DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty = null)
public Catcher([NotNull] DroppedObjectContainer droppedObjectTarget, IBeatmapDifficultyInfo difficulty = null)
{
this.droppedObjectTarget = droppedObjectTarget;
@@ -172,7 +177,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// <summary>
/// Calculates the scale of the catcher based off the provided beatmap difficulty.
/// </summary>
private static Vector2 calculateScale(BeatmapDifficulty difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
private static Vector2 calculateScale(IBeatmapDifficultyInfo difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
/// <summary>
/// Calculates the width of the area used for attempting catches in gameplay.
@@ -184,7 +189,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// Calculates the width of the area used for attempting catches in gameplay.
/// </summary>
/// <param name="difficulty">The beatmap difficulty.</param>
public static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty));
public static float CalculateCatchWidth(IBeatmapDifficultyInfo difficulty) => CalculateCatchWidth(calculateScale(difficulty));
/// <summary>
/// Determine if this catcher can catch a <see cref="CatchHitObject"/> in the current position.
@@ -226,11 +231,11 @@ namespace osu.Game.Rulesets.Catch.UI
if (result.IsHit && hitObject.HyperDash)
{
var target = hitObject.HyperDashTarget;
var timeDifference = target.StartTime - hitObject.StartTime;
double timeDifference = target.StartTime - hitObject.StartTime;
double positionDifference = target.EffectiveX - X;
var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
SetHyperDashState(Math.Abs(velocity), target.EffectiveX);
SetHyperDashState(Math.Abs(velocity) / BASE_DASH_SPEED, target.EffectiveX);
}
else
SetHyperDashState();
@@ -266,7 +271,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// <param name="targetPosition">When this catcher crosses this position, this catcher ends hyper-dashing.</param>
public void SetHyperDashState(double modifier = 1, float targetPosition = -1)
{
var wasHyperDashing = HyperDashing;
bool wasHyperDashing = HyperDashing;
if (modifier <= 1 || X == targetPosition)
{
@@ -27,14 +27,14 @@ namespace osu.Game.Rulesets.Catch.UI
: base(ruleset, beatmap, mods)
{
Direction.Value = ScrollingDirection.Down;
TimeRange.Value = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450);
TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450);
}
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield);
protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.Difficulty);
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchPlayfieldAdjustmentContainer();
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
{
var time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
var pos = column.ScreenSpacePositionAtTime(time);
return new SnapResult(pos, time, column);
@@ -13,6 +13,7 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
@@ -101,27 +102,27 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
throw new System.NotImplementedException();
}
public override float GetBeatSnapDistanceAt(double referenceTime)
public override float GetBeatSnapDistanceAt(HitObject referenceObject)
{
throw new System.NotImplementedException();
}
public override float DurationToDistance(double referenceTime, double duration)
public override float DurationToDistance(HitObject referenceObject, double duration)
{
throw new System.NotImplementedException();
}
public override double DistanceToDuration(double referenceTime, float distance)
public override double DistanceToDuration(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}
public override double GetSnappedDurationFromDistance(double referenceTime, float distance)
public override double GetSnappedDurationFromDistance(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}
public override float GetSnappedDistanceFromDistance(double referenceTime, float distance)
public override float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}
@@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Mania.Tests
public int CompareTo(ConvertValue other)
{
var result = StartTime.CompareTo(other.StartTime);
int result = StartTime.CompareTo(other.StartTime);
if (result != 0)
return result;
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Tests
private IList<string> getSampleNames(IList<HitSampleInfo> hitSampleInfo)
=> hitSampleInfo.Select(sample => sample.LookupNames.First()).ToList();
private IList<IList<string>> getNodeSampleNames(List<IList<HitSampleInfo>> hitSampleInfo)
private IList<IList<string>> getNodeSampleNames(IList<IList<HitSampleInfo>> hitSampleInfo)
=> hitSampleInfo?.Select(getSampleNames)
.ToList();
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Mania.Tests
private IEnumerable<ColumnType> getResults(StageDefinition definition)
{
for (var i = 0; i < definition.Columns; i++)
for (int i = 0; i < definition.Columns; i++)
yield return definition.GetTypeOfColumn(i);
}
}
@@ -388,7 +388,7 @@ namespace osu.Game.Rulesets.Mania.Tests
},
};
beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
}
AddStep("load player", () =>
@@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Mania.Tests
},
});
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
@@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
@@ -42,8 +42,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
{
IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
var roundedCircleSize = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
var roundedOverallDifficulty = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
double roundedCircleSize = Math.Round(beatmap.Difficulty.CircleSize);
double roundedOverallDifficulty = Math.Round(beatmap.Difficulty.OverallDifficulty);
if (IsForCurrentRuleset)
{
@@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
public static int GetColumnCountForNonConvert(BeatmapInfo beatmapInfo)
{
var roundedCircleSize = Math.Round(beatmapInfo.BaseDifficulty.CircleSize);
double roundedCircleSize = Math.Round(beatmapInfo.BaseDifficulty.CircleSize);
return (int)Math.Max(1, roundedCircleSize);
}
@@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{
BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty;
IBeatmapDifficultyInfo difficulty = original.Difficulty;
int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
Random = new FastRandom(seed);
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
Debug.Assert(distanceData != null);
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
DifficultyControlPoint difficultyPoint = hitObject.DifficultyControlPoint;
double beatLength;
#pragma warning disable 618
@@ -55,13 +55,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
#pragma warning restore 618
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
else
beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity;
SpanCount = repeatsData?.SpanCount() ?? 1;
StartTime = (int)Math.Round(hitObject.StartTime);
// This matches stable's calculation.
EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier);
EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.Difficulty.SliderMultiplier);
SegmentDuration = (EndTime - StartTime) / SpanCount;
}
@@ -488,12 +488,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// Retrieves the list of node samples that occur at time greater than or equal to <paramref name="time"/>.
/// </summary>
/// <param name="time">The time to retrieve node samples at.</param>
private List<IList<HitSampleInfo>> nodeSamplesAt(int time)
private IList<IList<HitSampleInfo>> nodeSamplesAt(int time)
{
if (!(HitObject is IHasPathWithRepeats curveData))
return null;
var index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration;
int index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration;
// avoid slicing the list & creating copies, if at all possible.
return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList();
@@ -302,7 +302,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern();
int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out var addToCentre);
int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out bool addToCentre);
int columnLimit = (TotalColumns % 2 == 0 ? TotalColumns : TotalColumns - 1) / 2;
int nextColumn = GetRandomColumn(upperBound: columnLimit);
@@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (drainTime == 0)
drainTime = 10000;
BeatmapDifficulty difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty;
IBeatmapDifficultyInfo difficulty = OriginalBeatmap.Difficulty;
conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
conversionDifficulty = Math.Min(conversionDifficulty.Value, 12);
@@ -28,7 +28,12 @@ namespace osu.Game.Rulesets.Mania.Configuration
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
{
new TrackedSetting<double>(ManiaRulesetSetting.ScrollTime,
v => new SettingDescription(v, "Scroll Speed", $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / v)} ({v}ms)"))
scrollTime => new SettingDescription(
rawValue: scrollTime,
name: "Scroll Speed",
value: $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)} ({scrollTime}ms)"
)
)
};
}
@@ -41,14 +41,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
return new ManiaDifficultyAttributes { Mods = mods, Skills = skills };
HitWindows hitWindows = new ManiaHitWindows();
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
return new ManiaDifficultyAttributes
{
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
Mods = mods,
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate),
GreatHitWindow = Math.Ceiling(getHitWindow300(mods) / clockRate),
ScoreMultiplier = getScoreMultiplier(mods),
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
Skills = skills
@@ -35,8 +35,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
var endTime = maniaCurrent.EndTime;
var column = maniaCurrent.BaseObject.Column;
double endTime = maniaCurrent.EndTime;
int column = maniaCurrent.BaseObject.Column;
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
+1 -1
View File
@@ -316,7 +316,7 @@ namespace osu.Game.Rulesets.Mania
case PlayfieldType.Dual:
{
var keys = getDualStageKeyCount(variant);
int keys = getDualStageKeyCount(variant);
return $"{keys}K + {keys}K";
}
}
@@ -62,9 +62,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils
if (i < j)
{
T key = keys[i];
keys[i] = keys[j];
keys[j] = key;
(keys[i], keys[j]) = (keys[j], keys[i]);
}
i++;
@@ -122,7 +120,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils
while (i <= n / 2)
{
var child = 2 * i;
int child = 2 * i;
if (child < n && comparer.Compare(keys[lo + child - 1], keys[lo + child]) < 0)
{
@@ -142,11 +140,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils
private static void swap(T[] a, int i, int j)
{
if (i != j)
{
T t = a[i];
a[i] = a[j];
a[j] = t;
}
(a[i], a[j]) = (a[j], a[i]);
}
private static void swapIfGreater(T[] keys, IComparer<T> comparer, int a, int b)
@@ -154,11 +148,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils
if (a != b)
{
if (comparer.Compare(keys[a], keys[b]) > 0)
{
T key = keys[a];
keys[a] = keys[b];
keys[b] = key;
}
(keys[a], keys[b]) = (keys[b], keys[a]);
}
}
}
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } },
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus" } },
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
};
}
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } },
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus" } },
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
};
}
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public void ApplyToBeatmap(IBeatmap beatmap)
{
var availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
int availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
beatmap.HitObjects.OfType<ManiaHitObject>().ForEach(h => h.Column = availableColumns - 1 - h.Column);
}
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Mods
Seed.Value ??= RNG.Next();
var rng = new Random((int)Seed.Value);
var availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
int availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(item => rng.Next()).ToList();
beatmap.HitObjects.OfType<ManiaHitObject>().ForEach(h => h.Column = shuffledColumns[h.Column]);
@@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (Time.Current < HitObject.StartTime)
return;
var startTime = holdStartTime?.Invoke();
double? startTime = holdStartTime?.Invoke();
if (startTime == null || startTime > HitObject.StartTime)
ApplyResult(r => r.Type = r.Judgement.MinResult);
+2 -2
View File
@@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Mania.Objects
}
}
public List<IList<HitSampleInfo>> NodeSamples { get; set; }
public IList<IList<HitSampleInfo>> NodeSamples { get; set; }
/// <summary>
/// The head note of the hold.
@@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Objects
/// </summary>
private double tickSpacing = 50;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
@@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Mania.Replays
{
var currentObject = Beatmap.HitObjects[i];
var nextObjectInColumn = GetNextObject(i); // Get the next object that requires pressing the same button
var releaseTime = calculateReleaseTime(currentObject, nextObjectInColumn);
double releaseTime = calculateReleaseTime(currentObject, nextObjectInColumn);
yield return new HitPoint { Time = currentObject.StartTime, Column = currentObject.Column };
@@ -13,6 +13,7 @@ SliderTickRate:1
[TimingPoints]
0,500,4,1,0,100,1,0
10000,-150,4,1,0,100,1,0
[HitObjects]
51,192,500,128,0,1500:1:0:0:0:
@@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
float rightLineWidth = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.RightLineWidth, columnIndex)?.Value ?? 1;
bool hasLeftLine = leftLineWidth > 0;
bool hasRightLine = rightLineWidth > 0 && skin.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m
bool hasRightLine = rightLineWidth > 0 && skin.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value >= 2.4m
|| isLastColumn;
Color4 lineColour = skin.GetManiaSkinConfig<Color4>(LegacyManiaSkinConfigurationLookups.ColumnLineColour, columnIndex)?.Value ?? Color4.White;
@@ -63,10 +63,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
this.beatmap = (ManiaBeatmap)beatmap;
isLegacySkin = new Lazy<bool>(() => GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version) != null);
isLegacySkin = new Lazy<bool>(() => GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version) != null);
hasKeyTexture = new Lazy<bool>(() =>
{
var keyImage = this.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value ?? "mania-key1";
string keyImage = this.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value ?? "mania-key1";
return this.GetAnimation(keyImage, true, true) != null;
});
}
@@ -90,11 +90,11 @@ namespace osu.Game.Rulesets.Mania.UI
{
// Mania doesn't care about global velocity
p.Velocity = 1;
p.BaseBeatLength *= Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier;
p.BaseBeatLength *= Beatmap.Difficulty.SliderMultiplier;
// For non-mania beatmap, speed changes should only happen through timing points
if (!isForCurrentRuleset)
p.DifficultyPoint = new DifficultyControlPoint();
p.EffectPoint = new EffectControlPoint();
}
BarLines.ForEach(Playfield.Add);
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
public class CheckTooShortSpinnersTest
{
private CheckTooShortSpinners check;
private BeatmapDifficulty difficulty;
private IBeatmapDifficultyInfo difficulty;
[SetUp]
public void Setup()
@@ -81,12 +81,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
assertTooShort(new List<HitObject> { spinnerHighOd }, difficultyHighOd);
}
private void assertOk(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
private void assertOk(List<HitObject> hitObjects, IBeatmapDifficultyInfo beatmapDifficulty)
{
Assert.That(check.Run(getContext(hitObjects, beatmapDifficulty)), Is.Empty);
}
private void assertVeryShort(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
private void assertVeryShort(List<HitObject> hitObjects, IBeatmapDifficultyInfo beatmapDifficulty)
{
var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList();
@@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateVeryShort);
}
private void assertTooShort(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
private void assertTooShort(List<HitObject> hitObjects, IBeatmapDifficultyInfo beatmapDifficulty)
{
var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList();
@@ -102,12 +102,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateTooShort);
}
private BeatmapVerifierContext getContext(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
private BeatmapVerifierContext getContext(List<HitObject> hitObjects, IBeatmapDifficultyInfo beatmapDifficulty)
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = hitObjects,
BeatmapInfo = new BeatmapInfo { BaseDifficulty = beatmapDifficulty }
BeatmapInfo = new BeatmapInfo { BaseDifficulty = new BeatmapDifficulty(beatmapDifficulty) }
};
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
@@ -11,6 +11,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects;
@@ -45,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[SetUp]
public void Setup() => Schedule(() =>
{
editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
editorBeatmap.Difficulty.SliderMultiplier = 1;
editorBeatmap.ControlPointInfo.Clear();
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
@@ -179,15 +180,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length;
public float GetBeatSnapDistanceAt(HitObject referenceObject) => (float)beat_length;
public float DurationToDistance(double referenceTime, double duration) => (float)duration;
public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration;
public double DistanceToDuration(double referenceTime, float distance) => distance;
public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0;
public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0;
public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0;
}
}
}
@@ -45,8 +45,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{
new Spinner
{
Duration = 2000,
Position = OsuPlayfield.BASE_SIZE / 2
Duration = 6000,
Position = OsuPlayfield.BASE_SIZE / 2,
}
}
},
@@ -117,10 +117,46 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
PassCondition = checkSomeHit
});
[Test]
public void TestApproachCirclesOnly() => CreateModTest(new ModTestData
{
Mod = new OsuModHidden { OnlyFadeApproachCircles = { Value = true } },
Autoplay = true,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 1000,
Position = new Vector2(206, 142)
},
new HitCircle
{
StartTime = 2000,
Position = new Vector2(306, 142)
},
new Slider
{
StartTime = 3000,
Position = new Vector2(156, 242),
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(200, 0), })
},
new Spinner
{
Position = new Vector2(256, 192),
StartTime = 7000,
EndTime = 9000
}
}
},
PassCondition = checkSomeHit
});
private bool checkSomeHit() => Player.ScoreProcessor.JudgedHits >= 4;
private bool objectWithIncreasedVisibilityHasIndex(int index)
=> Player.Mods.Value.OfType<TestOsuModHidden>().Single().FirstObject == Player.GameplayState.Beatmap.HitObjects[index];
=> Player.GameplayState.Mods.OfType<TestOsuModHidden>().Single().FirstObject == Player.GameplayState.Beatmap.HitObjects[index];
private class TestOsuModHidden : OsuModHidden
{
@@ -0,0 +1,150 @@
// 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.Utils;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModNoScope : OsuModTestScene
{
[Test]
public void TestVisibleDuringBreak()
{
CreateModTest(new ModTestData
{
Mod = new OsuModNoScope
{
HiddenComboCount = { Value = 0 },
},
Autoplay = true,
PassCondition = () => true,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
Position = new Vector2(300, 192),
StartTime = 1000,
},
new HitCircle
{
Position = new Vector2(300, 192),
StartTime = 5000,
}
},
Breaks = new List<BreakPeriod>
{
new BreakPeriod(2000, 4000),
}
}
});
AddUntilStep("wait for cursor to hide", () => cursorAlphaAlmostEquals(0));
AddUntilStep("wait for start of break", isBreak);
AddUntilStep("wait for cursor to show", () => cursorAlphaAlmostEquals(1));
AddUntilStep("wait for end of break", () => !isBreak());
AddUntilStep("wait for cursor to hide", () => cursorAlphaAlmostEquals(0));
}
[Test]
public void TestVisibleDuringSpinner()
{
CreateModTest(new ModTestData
{
Mod = new OsuModNoScope
{
HiddenComboCount = { Value = 0 },
},
Autoplay = true,
PassCondition = () => true,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
Position = new Vector2(300, 192),
StartTime = 1000,
},
new Spinner
{
Position = new Vector2(256, 192),
StartTime = 2000,
Duration = 2000,
},
new HitCircle
{
Position = new Vector2(300, 192),
StartTime = 5000,
}
}
}
});
AddUntilStep("wait for cursor to hide", () => cursorAlphaAlmostEquals(0));
AddUntilStep("wait for start of spinner", isSpinning);
AddUntilStep("wait for cursor to show", () => cursorAlphaAlmostEquals(1));
AddUntilStep("wait for end of spinner", () => !isSpinning());
AddUntilStep("wait for cursor to hide", () => cursorAlphaAlmostEquals(0));
}
[Test]
public void TestVisibleAfterComboBreak()
{
CreateModTest(new ModTestData
{
Mod = new OsuModNoScope
{
HiddenComboCount = { Value = 2 },
},
Autoplay = true,
PassCondition = () => true,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
Position = new Vector2(100, 192),
StartTime = 1000,
},
new HitCircle
{
Position = new Vector2(150, 192),
StartTime = 3000,
},
new HitCircle
{
Position = new Vector2(200, 192),
StartTime = 5000,
},
}
}
});
AddAssert("cursor must start visible", () => cursorAlphaAlmostEquals(1));
AddUntilStep("wait for combo", () => Player.ScoreProcessor.Combo.Value >= 2);
AddAssert("cursor must dim after combo", () => !cursorAlphaAlmostEquals(1));
AddStep("break combo", () => Player.ScoreProcessor.Combo.Set(0));
AddUntilStep("wait for cursor to show", () => cursorAlphaAlmostEquals(1));
}
private bool isSpinning() => Player.ChildrenOfType<DrawableSpinner>().SingleOrDefault()?.Progress > 0;
private bool isBreak() => Player.IsBreakTime.Value;
private bool cursorAlphaAlmostEquals(float alpha) => Precision.AlmostEquals(Player.DrawableRuleset.Cursor.Alpha, alpha);
}
}
@@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.6634445062299665d, "diffcalc-test")]
[TestCase(1.0414203870195022d, "zero-length-sliders")]
[TestCase(6.5295339534769958d, "diffcalc-test")]
[TestCase(1.1514260533755143d, "zero-length-sliders")]
public void Test(double expected, string name)
=> base.Test(expected, name);
[TestCase(8.3858089051603368d, "diffcalc-test")]
[TestCase(1.2723279173428435d, "zero-length-sliders")]
[TestCase(9.047752485219954d, "diffcalc-test")]
[TestCase(1.3985711787077566d, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new OsuModDoubleTime());
@@ -59,8 +59,8 @@ namespace osu.Game.Rulesets.Osu.Tests
AddSliderStep("circle size", 0f, 10f, 0f, val =>
{
config.SetValue(OsuSetting.AutoCursorSize, true);
gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = val;
Scheduler.AddOnce(() => loadContent(false));
gameplayState.Beatmap.Difficulty.CircleSize = val;
Scheduler.AddOnce(loadContent);
});
AddStep("test cursor container", () => loadContent(false));
@@ -75,10 +75,10 @@ namespace osu.Game.Rulesets.Osu.Tests
public void TestSizing(int circleSize, float userScale)
{
AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale));
AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize);
AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.Difficulty.CircleSize = circleSize);
AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true));
AddStep("load content", () => loadContent());
AddStep("load content", loadContent);
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale);
@@ -98,7 +98,9 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("load content", () => loadContent(false, () => new SkinProvidingContainer(new TopLeftCursorSkin())));
}
private void loadContent(bool automated = true, Func<SkinProvidingContainer> skinProvider = null)
private void loadContent() => loadContent(false);
private void loadContent(bool automated, Func<SkinProvidingContainer> skinProvider = null)
{
SetContents(_ =>
{
@@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void scheduleHit() => AddStep("schedule action", () =>
{
var delay = hitCircle.StartTime - hitCircle.HitWindows.WindowFor(HitResult.Great) - Time.Current;
double delay = hitCircle.StartTime - hitCircle.HitWindows.WindowFor(HitResult.Great) - Time.Current;
Scheduler.AddDelayed(() => hitAreaReceptor.OnPressed(new KeyBindingPressEvent<OsuAction>(GetContainingInputManager().CurrentState, OsuAction.LeftButton)), delay);
});
}
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests
};
var hitWindows = new OsuHitWindows();
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
CreateModTest(new ModTestData
{
@@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests
};
var hitWindows = new OsuHitWindows();
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
CreateModTest(new ModTestData
{
@@ -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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class TestSceneNoSpinnerStacking : TestSceneOsuPlayer
{
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap
{
BeatmapInfo = new BeatmapInfo
{
BaseDifficulty = new BeatmapDifficulty { OverallDifficulty = 10 },
Ruleset = ruleset
}
};
for (int i = 0; i < 512; i++)
{
if (i % 32 < 20)
beatmap.HitObjects.Add(new Spinner { Position = new Vector2(256, 192), StartTime = i * 200, EndTime = (i * 200) + 100 });
}
return beatmap;
}
}
}
@@ -400,15 +400,13 @@ namespace osu.Game.Rulesets.Osu.Tests
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
{
HitObjects = hitObjects,
Difficulty = new BeatmapDifficulty { SliderTickRate = 3 },
BeatmapInfo =
{
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
Ruleset = new OsuRuleset().RulesetInfo
},
});
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
SelectedMods.Value = new[] { new OsuModClassic() };
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
@@ -439,6 +437,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public TestSlider()
{
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
DefaultsApplied += _ =>
{
HeadCircle.HitWindows = new TestHitWindows();
@@ -452,7 +452,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private class TestSpinner : Spinner
{
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
SpinsRequired = 1;
@@ -13,6 +13,7 @@ using osuTK.Graphics;
using osu.Game.Rulesets.Mods;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
@@ -328,10 +329,14 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier)
{
var cpi = new ControlPointInfo();
cpi.Add(0, new DifficultyControlPoint { SpeedMultiplier = speedMultiplier });
var cpi = new LegacyControlPointInfo();
cpi.Add(0, new DifficultyControlPoint { SliderVelocity = speedMultiplier });
slider.ApplyDefaults(cpi, new BeatmapDifficulty { CircleSize = circleSize, SliderTickRate = 3 });
slider.ApplyDefaults(cpi, new BeatmapDifficulty
{
CircleSize = circleSize,
SliderTickRate = 3
});
var drawable = CreateDrawableSlider(slider);
@@ -348,6 +348,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider_start,
Position = new Vector2(0, 0),
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f },
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
@@ -362,8 +363,6 @@ namespace osu.Game.Rulesets.Osu.Tests
},
});
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
@@ -30,6 +30,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneSpinnerRotation : TestSceneOsuPlayer
{
private const double spinner_start_time = 100;
private const double spinner_duration = 6000;
[Resolved]
private AudioManager audioManager { get; set; }
@@ -77,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests
double finalTrackerRotation = 0, trackerRotationTolerance = 0;
double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
addSeekStep(5000);
addSeekStep(spinner_start_time + 5000);
AddStep("retrieve disc rotation", () =>
{
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
@@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests
});
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation);
addSeekStep(2500);
addSeekStep(spinner_start_time + 2500);
AddAssert("disc rotation rewound",
// we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
// due to the exponential damping applied we're allowing a larger margin of error of about 10%
@@ -102,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Tests
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
() => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100));
addSeekStep(5000);
addSeekStep(spinner_start_time + 5000);
AddAssert("is disc rotation almost same",
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance));
AddAssert("is symbol rotation almost same",
@@ -140,12 +143,12 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestSpinnerNormalBonusRewinding()
{
addSeekStep(1000);
addSeekStep(spinner_start_time + 1000);
AddAssert("player score matching expected bonus score", () =>
{
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
double totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
return totalScore == (int)(drawableSpinner.Result.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult;
});
@@ -201,24 +204,9 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
}
private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
{
Frames = scoreReplay
.Frames
.Cast<OsuReplayFrame>()
.Select(replayFrame =>
{
var adjustedTime = replayFrame.Time * rate;
return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray());
})
.Cast<ReplayFrame>()
.ToList()
};
private void addSeekStep(double time)
{
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
@@ -241,7 +229,8 @@ namespace osu.Game.Rulesets.Osu.Tests
new Spinner
{
Position = new Vector2(256, 192),
EndTime = 6000,
StartTime = spinner_start_time,
Duration = spinner_duration
},
}
};
@@ -369,8 +369,6 @@ namespace osu.Game.Rulesets.Osu.Tests
},
});
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
@@ -399,6 +397,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public TestSlider()
{
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
DefaultsApplied += _ =>
{
HeadCircle.HitWindows = new TestHitWindows();
@@ -412,7 +412,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private class TestSpinner : Spinner
{
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
SpinsRequired = 1;
@@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
@@ -11,6 +11,7 @@ using System.Linq;
using System.Threading;
using osu.Game.Rulesets.Osu.UI;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Beatmaps.Legacy;
namespace osu.Game.Rulesets.Osu.Beatmaps
{
@@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / beatmap.ControlPointInfo.DifficultyPointAt(original.StartTime).SpeedMultiplier : 1
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1
}.Yield();
case IHasDuration endTimeData:
@@ -12,7 +12,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
public double FlashlightRating { get; set; }
public double ApproachRate { get; set; }
public double OverallDifficulty { get; set; }
public double DrainRate { get; set; }
public int HitCircleCount { get; set; }
public int SliderCount { get; set; }
public int SpinnerCount { get; set; }
}
}
@@ -37,6 +37,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
double flashlightRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
if (mods.Any(h => h is OsuModRelax))
speedRating = 0.0;
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
double baseFlashlightPerformance = 0.0;
@@ -53,13 +56,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate;
int maxCombo = beatmap.HitObjects.Count;
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
return new OsuDifficultyAttributes
@@ -71,8 +76,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
FlashlightRating = flashlightRating,
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6,
DrainRate = drainRate,
MaxCombo = maxCombo,
HitCircleCount = hitCirclesCount,
SliderCount = sliderCount,
SpinnerCount = spinnerCount,
Skills = skills
};
@@ -95,10 +102,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
{
HitWindows hitWindows = new OsuHitWindows();
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
// Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future
hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate;
hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
return new Skill[]
{
@@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private int countMeh;
private int countMiss;
private int effectiveMissCount;
public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
: base(ruleset, attributes, score)
{
@@ -39,16 +41,23 @@ namespace osu.Game.Rulesets.Osu.Difficulty
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
effectiveMissCount = calculateEffectiveMissCount();
// Custom multipliers for NoFail and SpunOut.
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
// Custom multipliers for NoFail and SpunOut.
if (mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss);
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
if (mods.Any(m => m is OsuModSpunOut))
multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85);
if (mods.Any(h => h is OsuModRelax))
{
effectiveMissCount += countOk + countMeh;
multiplier *= 0.6;
}
double aimValue = computeAimValue();
double speedValue = computeSpeedValue();
double accuracyValue = computeAccuracyValue();
@@ -91,8 +100,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= lengthBonus;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (countMiss > 0)
aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), countMiss);
if (effectiveMissCount > 0)
aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), effectiveMissCount);
// Combo scaling.
if (Attributes.MaxCombo > 0)
@@ -108,9 +117,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
if (mods.Any(h => h is OsuModHidden))
if (mods.Any(m => m is OsuModBlinds))
aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate);
else if (mods.Any(h => h is OsuModHidden))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
}
aimValue *= approachRateBonus;
@@ -132,8 +145,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= lengthBonus;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (countMiss > 0)
speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875));
if (effectiveMissCount > 0)
speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
// Combo scaling.
if (Attributes.MaxCombo > 0)
@@ -147,11 +160,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
if (mods.Any(m => m is OsuModHidden))
if (mods.Any(m => m is OsuModBlinds))
{
// Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.
speedValue *= 1.12;
}
else if (mods.Any(m => m is OsuModHidden))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
}
// Scale the speed value with accuracy and OD.
speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
@@ -160,6 +182,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeAccuracyValue()
{
if (mods.Any(h => h is OsuModRelax))
return 0.0;
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
double betterAccuracyPercentage;
int amountHitObjectsWithAccuracy = Attributes.HitCircleCount;
@@ -180,8 +205,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
if (mods.Any(m => m is OsuModHidden))
// Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
if (mods.Any(m => m is OsuModBlinds))
accuracyValue *= 1.14;
else if (mods.Any(m => m is OsuModHidden))
accuracyValue *= 1.08;
if (mods.Any(m => m is OsuModFlashlight))
accuracyValue *= 1.02;
@@ -205,8 +234,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
flashlightValue *= 1.3;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (countMiss > 0)
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875));
if (effectiveMissCount > 0)
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
// Combo scaling.
if (Attributes.MaxCombo > 0)
@@ -224,6 +253,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return flashlightValue;
}
private int calculateEffectiveMissCount()
{
// guess the number of misses + slider breaks from combo
double comboBasedMissCount = 0.0;
if (Attributes.SliderCount > 0)
{
double fullComboThreshold = Attributes.MaxCombo - 0.1 * Attributes.SliderCount;
if (scoreMaxCombo < fullComboThreshold)
comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
}
// we're clamping misscount because since its derived from combo it can be higher than total hits and that breaks some calculations
comboBasedMissCount = Math.Min(comboBasedMissCount, totalHits);
return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount));
}
private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh;
}
@@ -12,20 +12,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{
public class OsuDifficultyHitObject : DifficultyHitObject
{
private const int normalized_radius = 52;
private const int normalized_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
private const int min_delta_time = 25;
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms to account for simultaneous <see cref="OsuDifficultyHitObject"/>s.
/// </summary>
public double StrainTime { get; private set; }
/// <summary>
/// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
/// </summary>
public double JumpDistance { get; private set; }
/// <summary>
/// Minimum distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
/// </summary>
public double MovementDistance { get; private set; }
/// <summary>
/// Normalized distance between the start and end position of the previous <see cref="OsuDifficultyHitObject"/>.
/// </summary>
@@ -37,6 +38,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
/// </summary>
public double? Angle { get; private set; }
/// <summary>
/// Milliseconds elapsed since the end time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public double MovementTime { get; private set; }
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/> to the end time of the same previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public double TravelTime { get; private set; }
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public readonly double StrainTime;
private readonly OsuHitObject lastLastObject;
private readonly OsuHitObject lastObject;
@@ -46,14 +62,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
this.lastLastObject = (OsuHitObject)lastLastObject;
this.lastObject = (OsuHitObject)lastObject;
setDistances();
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
StrainTime = Math.Max(DeltaTime, min_delta_time);
// Capped to 25ms to prevent difficulty calculation breaking from simulatenous objects.
StrainTime = Math.Max(DeltaTime, 25);
setDistances(clockRate);
}
private void setDistances()
private void setDistances(double clockRate)
{
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
if (BaseObject is Spinner || lastObject is Spinner)
return;
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
float scalingFactor = normalized_radius / (float)BaseObject.Radius;
@@ -63,19 +83,31 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
scalingFactor *= 1 + smallCircleBonus;
}
Vector2 lastCursorPosition = getEndCursorPosition(lastObject);
JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
if (lastObject is Slider lastSlider)
{
computeSliderCursorPosition(lastSlider);
TravelDistance = lastSlider.LazyTravelDistance * scalingFactor;
TravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time);
MovementTime = Math.Max(StrainTime - TravelTime, min_delta_time);
// Jump distance from the slider tail to the next object, as opposed to the lazy position of JumpDistance.
float tailJumpDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor;
// For hitobjects which continue in the direction of the slider, the player will normally follow through the slider,
// such that they're not jumping from the lazy position but rather from very close to (or the end of) the slider.
// In such cases, a leniency is applied by also considering the jump distance from the tail of the slider, and taking the minimum jump distance.
MovementDistance = Math.Min(JumpDistance, tailJumpDistance);
}
else
{
MovementTime = StrainTime;
MovementDistance = JumpDistance;
}
Vector2 lastCursorPosition = getEndCursorPosition(lastObject);
// Don't need to jump to reach spinners
if (!(BaseObject is Spinner))
JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
if (lastLastObject != null)
if (lastLastObject != null && !(lastLastObject is Spinner))
{
Vector2 lastLastCursorPosition = getEndCursorPosition(lastLastObject);
@@ -96,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
slider.LazyEndPosition = slider.StackedPosition;
float approxFollowCircleRadius = (float)(slider.Radius * 3);
float followCircleRadius = (float)(slider.Radius * 2.4);
var computeVertex = new Action<double>(t =>
{
double progress = (t - slider.StartTime) / slider.SpanDuration;
@@ -109,11 +141,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
var diff = slider.StackedPosition + slider.Path.PositionAt(progress) - slider.LazyEndPosition.Value;
float dist = diff.Length;
if (dist > approxFollowCircleRadius)
slider.LazyTravelTime = t - slider.StartTime;
if (dist > followCircleRadius)
{
// The cursor would be outside the follow circle, we need to move it
diff.Normalize(); // Obtain direction of diff
dist -= approxFollowCircleRadius;
dist -= followCircleRadius;
slider.LazyEndPosition += diff * dist;
slider.LazyTravelDistance += dist;
}
@@ -121,7 +155,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
// Skip the head circle
var scoringTimes = slider.NestedHitObjects.Skip(1).Select(t => t.StartTime);
foreach (var time in scoringTimes)
foreach (double time in scoringTimes)
computeVertex(time);
}
+81 -24
View File
@@ -14,51 +14,108 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// </summary>
public class Aim : OsuStrainSkill
{
private const double angle_bonus_begin = Math.PI / 3;
private const double timing_threshold = 107;
public Aim(Mod[] mods)
: base(mods)
{
}
protected override double SkillMultiplier => 26.25;
protected override double StrainDecayBase => 0.15;
protected override int HistoryLength => 2;
protected override double StrainValueOf(DifficultyHitObject current)
private const double wide_angle_multiplier = 1.5;
private const double acute_angle_multiplier = 2.0;
private double currentStrain = 1;
private double skillMultiplier => 23.25;
private double strainDecayBase => 0.15;
private double strainValueOf(DifficultyHitObject current)
{
if (current.BaseObject is Spinner)
if (current.BaseObject is Spinner || Previous.Count <= 1 || Previous[0].BaseObject is Spinner)
return 0;
var osuCurrent = (OsuDifficultyHitObject)current;
var osuCurrObj = (OsuDifficultyHitObject)current;
var osuLastObj = (OsuDifficultyHitObject)Previous[0];
var osuLastLastObj = (OsuDifficultyHitObject)Previous[1];
double result = 0;
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
double currVelocity = osuCurrObj.JumpDistance / osuCurrObj.StrainTime;
if (Previous.Count > 0)
// But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
if (osuLastObj.BaseObject is Slider)
{
var osuPrevious = (OsuDifficultyHitObject)Previous[0];
double movementVelocity = osuCurrObj.MovementDistance / osuCurrObj.MovementTime; // calculate the movement velocity from slider end to current object
double travelVelocity = osuCurrObj.TravelDistance / osuCurrObj.TravelTime; // calculate the slider velocity from slider head to slider end.
if (osuCurrent.Angle != null && osuCurrent.Angle.Value > angle_bonus_begin)
currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity.
}
// As above, do the same for the previous hitobject.
double prevVelocity = osuLastObj.JumpDistance / osuLastObj.StrainTime;
if (osuLastLastObj.BaseObject is Slider)
{
double movementVelocity = osuLastObj.MovementDistance / osuLastObj.MovementTime;
double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime;
prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity);
}
double angleBonus = 0;
double aimStrain = currVelocity; // Start strain with regular velocity.
if (Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime) < 1.25 * Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime)) // If rhythms are the same.
{
if (osuCurrObj.Angle != null && osuLastObj.Angle != null && osuLastLastObj.Angle != null)
{
const double scale = 90;
double currAngle = osuCurrObj.Angle.Value;
double lastAngle = osuLastObj.Angle.Value;
double lastLastAngle = osuLastLastObj.Angle.Value;
var angleBonus = Math.Sqrt(
Math.Max(osuPrevious.JumpDistance - scale, 0)
* Math.Pow(Math.Sin(osuCurrent.Angle.Value - angle_bonus_begin), 2)
* Math.Max(osuCurrent.JumpDistance - scale, 0));
result = 1.4 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime);
// Rewarding angles, take the smaller velocity as base.
angleBonus = Math.Min(currVelocity, prevVelocity);
double wideAngleBonus = calcWideAngleBonus(currAngle);
double acuteAngleBonus = calcAcuteAngleBonus(currAngle);
if (osuCurrObj.StrainTime > 100) // Only buff deltaTime exceeding 300 bpm 1/2.
acuteAngleBonus = 0;
else
{
acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
* Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.JumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter).
}
wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3))); // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
acuteAngleBonus *= 0.5 + 0.5 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3))); // Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse.
angleBonus = acuteAngleBonus * acute_angle_multiplier + wideAngleBonus * wide_angle_multiplier; // add the angle buffs together.
}
}
double jumpDistanceExp = applyDiminishingExp(osuCurrent.JumpDistance);
double travelDistanceExp = applyDiminishingExp(osuCurrent.TravelDistance);
aimStrain += angleBonus; // Add in angle bonus.
return Math.Max(
result + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(osuCurrent.StrainTime, timing_threshold),
(Math.Sqrt(travelDistanceExp * jumpDistanceExp) + jumpDistanceExp + travelDistanceExp) / osuCurrent.StrainTime
);
return aimStrain;
}
private double calcWideAngleBonus(double angle) => Math.Pow(Math.Sin(3.0 / 4 * (Math.Min(5.0 / 6 * Math.PI, Math.Max(Math.PI / 6, angle)) - Math.PI / 6)), 2);
private double calcAcuteAngleBonus(double angle) => 1 - calcWideAngleBonus(angle);
private double applyDiminishingExp(double val) => Math.Pow(val, 0.99);
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
protected override double CalculateInitialStrain(double time) => currentStrain * strainDecay(time - Previous[0].StartTime);
protected override double StrainValueAt(DifficultyHitObject current)
{
currentStrain *= strainDecay(current.DeltaTime);
currentStrain += strainValueOf(current) * skillMultiplier;
return currentStrain;
}
}
}
@@ -19,12 +19,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
}
protected override double SkillMultiplier => 0.15;
protected override double StrainDecayBase => 0.15;
private double skillMultiplier => 0.15;
private double strainDecayBase => 0.15;
protected override double DecayWeight => 1.0;
protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations.
private double currentStrain = 1;
protected override double StrainValueOf(DifficultyHitObject current)
private double strainValueOf(DifficultyHitObject current)
{
if (current.BaseObject is Spinner)
return 0;
@@ -62,5 +63,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return Math.Pow(smallDistNerf * result, 2.0);
}
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
protected override double CalculateInitialStrain(double time) => currentStrain * strainDecay(time - Previous[0].StartTime);
protected override double StrainValueAt(DifficultyHitObject current)
{
currentStrain *= strainDecay(current.DeltaTime);
currentStrain += strainValueOf(current) * skillMultiplier;
return currentStrain;
}
}
}
@@ -10,7 +10,7 @@ using osu.Framework.Utils;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
public abstract class OsuStrainSkill : StrainDecaySkill
public abstract class OsuStrainSkill : StrainSkill
{
/// <summary>
/// The number of sections with the highest strains, which the peak strain reductions will apply to.
+123 -35
View File
@@ -16,19 +16,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
public class Speed : OsuStrainSkill
{
private const double single_spacing_threshold = 125;
private const double angle_bonus_begin = 5 * Math.PI / 6;
private const double pi_over_4 = Math.PI / 4;
private const double pi_over_2 = Math.PI / 2;
protected override double SkillMultiplier => 1400;
protected override double StrainDecayBase => 0.3;
protected override int ReducedSectionCount => 5;
protected override double DifficultyMultiplier => 1.04;
private const double rhythm_multiplier = 0.75;
private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max.
private const double min_speed_bonus = 75; // ~200BPM
private const double speed_balancing_factor = 40;
private double skillMultiplier => 1375;
private double strainDecayBase => 0.3;
private double currentStrain = 1;
private double currentRhythm = 1;
protected override int ReducedSectionCount => 5;
protected override double DifficultyMultiplier => 1.04;
protected override int HistoryLength => 32;
private readonly double greatWindow;
public Speed(Mod[] mods, double hitWindowGreat)
@@ -37,52 +39,138 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
greatWindow = hitWindowGreat;
}
protected override double StrainValueOf(DifficultyHitObject current)
/// <summary>
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
/// </summary>
private double calculateRhythmBonus(DifficultyHitObject current)
{
if (current.BaseObject is Spinner)
return 0;
var osuCurrent = (OsuDifficultyHitObject)current;
var osuPrevious = Previous.Count > 0 ? (OsuDifficultyHitObject)Previous[0] : null;
int previousIslandSize = 0;
double distance = Math.Min(single_spacing_threshold, osuCurrent.TravelDistance + osuCurrent.JumpDistance);
double strainTime = osuCurrent.StrainTime;
double rhythmComplexitySum = 0;
int islandSize = 1;
double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms
bool firstDeltaSwitch = false;
for (int i = Previous.Count - 2; i > 0; i--)
{
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)Previous[i - 1];
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)Previous[i];
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)Previous[i + 1];
double currHistoricalDecay = Math.Max(0, (history_time_max - (current.StartTime - currObj.StartTime))) / history_time_max; // scales note 0 to 1 from history to now
if (currHistoricalDecay != 0)
{
currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count.
double currDelta = currObj.StrainTime;
double prevDelta = prevObj.StrainTime;
double lastDelta = lastObj.StrainTime;
double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6));
windowPenalty = Math.Min(1, windowPenalty);
double effectiveRatio = windowPenalty * currRatio;
if (firstDeltaSwitch)
{
if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
{
if (islandSize < 7)
islandSize++; // island is still progressing, count size.
}
else
{
if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window
effectiveRatio *= 0.125;
if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
effectiveRatio *= 0.25;
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
effectiveRatio *= 0.25;
if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
effectiveRatio *= 0.50;
if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
effectiveRatio *= 0.125;
rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2;
startRatio = effectiveRatio;
previousIslandSize = islandSize; // log the last island size.
if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
islandSize = 1;
}
}
else if (prevDelta > 1.25 * currDelta) // we want to be speeding up.
{
// Begin counting island until we change speed again.
firstDeltaSwitch = true;
startRatio = effectiveRatio;
islandSize = 1;
}
}
}
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)
}
private double strainValueOf(DifficultyHitObject current)
{
if (current.BaseObject is Spinner)
return 0;
// derive strainTime for calculation
var osuCurrObj = (OsuDifficultyHitObject)current;
var osuPrevObj = Previous.Count > 0 ? (OsuDifficultyHitObject)Previous[0] : null;
double strainTime = osuCurrObj.StrainTime;
double greatWindowFull = greatWindow * 2;
double speedWindowRatio = strainTime / greatWindowFull;
// Aim to nerf cheesy rhythms (Very fast consecutive doubles with large deltatimes between)
if (osuPrevious != null && strainTime < greatWindowFull && osuPrevious.StrainTime > strainTime)
strainTime = Interpolation.Lerp(osuPrevious.StrainTime, strainTime, speedWindowRatio);
if (osuPrevObj != null && strainTime < greatWindowFull && osuPrevObj.StrainTime > strainTime)
strainTime = Interpolation.Lerp(osuPrevObj.StrainTime, strainTime, speedWindowRatio);
// Cap deltatime to the OD 300 hitwindow.
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
strainTime /= Math.Clamp((strainTime / greatWindowFull) / 0.93, 0.92, 1);
// derive speedBonus for calculation
double speedBonus = 1.0;
if (strainTime < min_speed_bonus)
speedBonus = 1 + Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
double angleBonus = 1.0;
double distance = Math.Min(single_spacing_threshold, osuCurrObj.TravelDistance + osuCurrObj.JumpDistance);
if (osuCurrent.Angle != null && osuCurrent.Angle.Value < angle_bonus_begin)
{
angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - osuCurrent.Angle.Value)), 2) / 3.57;
return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime;
}
if (osuCurrent.Angle.Value < pi_over_2)
{
angleBonus = 1.28;
if (distance < 90 && osuCurrent.Angle.Value < pi_over_4)
angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1);
else if (distance < 90)
angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1) * Math.Sin((pi_over_2 - osuCurrent.Angle.Value) / pi_over_4);
}
}
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
return (1 + (speedBonus - 1) * 0.75)
* angleBonus
* (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5))
/ strainTime;
protected override double CalculateInitialStrain(double time) => (currentStrain * currentRhythm) * strainDecay(time - Previous[0].StartTime);
protected override double StrainValueAt(DifficultyHitObject current)
{
currentStrain *= strainDecay(current.DeltaTime);
currentStrain += strainValueOf(current) * skillMultiplier;
currentRhythm = calculateRhythmBonus(current);
return currentStrain * currentRhythm;
}
}
}
@@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
Vector2[] oldControlPoints = slider.Path.ControlPoints.Select(cp => cp.Position).ToArray();
var oldPosition = slider.Position;
var oldStartTime = slider.StartTime;
double oldStartTime = slider.StartTime;
if (ControlPoint == slider.Path.ControlPoints[0])
{
@@ -205,7 +205,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (!slider.Path.HasValidLength)
{
for (var i = 0; i < slider.Path.ControlPoints.Count; i++)
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Position = oldControlPoints[i];
slider.Position = oldPosition;
@@ -8,12 +8,14 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Screens.Edit;
using osuTK;
using osuTK.Input;
@@ -67,6 +69,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
inputManager = GetContainingInputManager();
}
[Resolved]
private EditorBeatmap editorBeatmap { get; set; }
public override void UpdateTimeAndPosition(SnapResult result)
{
base.UpdateTimeAndPosition(result);
@@ -75,6 +80,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
case SliderPlacementState.Initial:
BeginPlacement();
var nearestDifficultyPoint = editorBeatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < HitObject.StartTime)?.DifficultyControlPoint?.DeepClone() as DifficultyControlPoint;
HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint();
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
break;
@@ -212,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updateSlider()
{
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
bodyPiece.UpdateFrom(HitObject);
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
@@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updatePath()
{
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
editorBeatmap?.Update(HitObject);
}
@@ -47,14 +47,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks
if (!(hitObjects[i + 1] is OsuHitObject nextHitObject) || nextHitObject is Spinner)
continue;
var deltaTime = nextHitObject.StartTime - hitObject.GetEndTime();
double deltaTime = nextHitObject.StartTime - hitObject.GetEndTime();
if (deltaTime >= hitObject.TimeFadeIn + hitObject.TimePreempt)
// The objects are not visible at the same time (without mods), hence skipping.
continue;
var distanceSq = (hitObject.StackedEndPosition - nextHitObject.StackedPosition).LengthSquared;
var diameter = (hitObject.Radius - overlap_leniency) * 2;
var diameterSq = diameter * diameter;
float distanceSq = (hitObject.StackedEndPosition - nextHitObject.StackedPosition).LengthSquared;
double diameter = (hitObject.Radius - overlap_leniency) * 2;
double diameterSq = diameter * diameter;
bool areOverlapping = distanceSq < diameterSq;
@@ -88,21 +88,21 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks
if (!(hitObjects[i + 1] is OsuHitObject nextHitObject) || nextHitObject is Spinner)
continue;
var deltaTime = nextHitObject.StartTime - hitObject.GetEndTime();
double deltaTime = nextHitObject.StartTime - hitObject.GetEndTime();
// Ignore objects that are far enough apart in time to not be considered the same pattern.
if (deltaTime > pattern_lifetime)
continue;
// Relying on FastInvSqrt is probably good enough here. We'll be taking the difference between distances later, hence square not being sufficient.
var distance = (hitObject.StackedEndPosition - nextHitObject.StackedPosition).LengthFast;
float distance = (hitObject.StackedEndPosition - nextHitObject.StackedPosition).LengthFast;
// Ignore stacks and half-stacks, as these are close enough to where they can't be confused for being time-distanced.
if (distance < stack_leniency)
continue;
var observedTimeDistance = new ObservedTimeDistance(nextHitObject.StartTime, deltaTime, distance);
var expectedDistance = getExpectedDistance(prevObservedTimeDistances, observedTimeDistance);
double expectedDistance = getExpectedDistance(prevObservedTimeDistances, observedTimeDistance);
if (expectedDistance == 0)
{
@@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks
private double getExpectedDistance(IEnumerable<ObservedTimeDistance> prevObservedTimeDistances, ObservedTimeDistance observedTimeDistance)
{
var observations = prevObservedTimeDistances.Count();
int observations = prevObservedTimeDistances.Count();
int count = 0;
double sum = 0;

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