1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 11:37:28 +08:00

Merge remote-tracking branch 'upstream/master' into android-build-automation

This commit is contained in:
Dean Herbert 2019-09-24 16:18:35 +09:00
commit e97aa60487
98 changed files with 2142 additions and 401 deletions

View File

@ -51,7 +51,6 @@
<None Include="$(MSBuildThisFileDirectory)\osu.licenseheader"> <None Include="$(MSBuildThisFileDirectory)\osu.licenseheader">
<Link>osu.licenseheader</Link> <Link>osu.licenseheader</Link>
</None> </None>
<AndroidNativeLibrary Include="$(OutputPath)\**\*.so" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System" /> <Reference Include="System" />
@ -63,6 +62,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.913.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.913.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2019.911.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2019.921.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -23,10 +23,10 @@
<ProjectReference Include="..\osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" /> <PackageReference Include="Microsoft.Win32.Registry" Version="4.6.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="System.IO.Packaging" Version="4.5.0" /> <PackageReference Include="System.IO.Packaging" Version="4.6.0" />
<PackageReference Include="ppy.squirrel.windows" Version="1.9.0.4" /> <PackageReference Include="ppy.squirrel.windows" Version="1.9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.6" />

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -37,9 +37,6 @@ namespace osu.Game.Rulesets.Catch.Replays
float lastPosition = 0.5f; float lastPosition = 0.5f;
double lastTime = 0; double lastTime = 0;
// Todo: Realistically this shouldn't be needed, but the first frame is skipped with the way replays are currently handled
addFrame(-100000, lastPosition);
void moveToNext(CatchHitObject h) void moveToNext(CatchHitObject h)
{ {
float positionChange = Math.Abs(lastPosition - h.X); float positionChange = Math.Abs(lastPosition - h.X);
@ -127,6 +124,10 @@ namespace osu.Game.Rulesets.Catch.Replays
private void addFrame(double time, float? position = null, bool dashing = false) private void addFrame(double time, float? position = null, bool dashing = false)
{ {
// todo: can be removed once FramedReplayInputHandler correctly handles rewinding before first frame.
if (Replay.Frames.Count == 0)
Replay.Frames.Add(new CatchReplayFrame(time - 1, position, false, null));
var last = currentFrame; var last = currentFrame;
currentFrame = new CatchReplayFrame(time, position, dashing, last); currentFrame = new CatchReplayFrame(time, position, dashing, last);
Replay.Frames.Add(currentFrame); Replay.Frames.Add(currentFrame);

View File

@ -16,6 +16,11 @@ namespace osu.Game.Rulesets.Mania.Tests
[HeadlessTest] [HeadlessTest]
public class TestSceneAutoGeneration : OsuTestScene public class TestSceneAutoGeneration : OsuTestScene
{ {
/// <summary>
/// The number of frames which are generated at the start of a replay regardless of hitobject content.
/// </summary>
private const int frame_offset = 1;
[Test] [Test]
public void TestSingleNote() public void TestSingleNote()
{ {
@ -28,11 +33,11 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate(); var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time"); Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Special1), "Special1 has not been pressed"); Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Special1), "Special1 has not been released"); Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released");
} }
[Test] [Test]
@ -49,11 +54,11 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate(); var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time"); Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Special1), "Special1 has not been pressed"); Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Special1), "Special1 has not been released"); Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released");
} }
[Test] [Test]
@ -69,11 +74,11 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate(); var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time"); Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed"); Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released"); Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released");
} }
[Test] [Test]
@ -91,11 +96,13 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate(); var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time");
Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed"); Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released");
} }
[Test] [Test]
@ -112,15 +119,15 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate(); var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.IsTrue(generated.Frames.Count == 5, "Replay must have 5 frames"); Assert.IsTrue(generated.Frames.Count == frame_offset + 4, "Replay must have 4 generated frames");
Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect first note hit time"); Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect first note release time"); Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect first note release time");
Assert.AreEqual(2000, generated.Frames[3].Time, "Incorrect second note hit time"); Assert.AreEqual(2000, generated.Frames[frame_offset + 2].Time, "Incorrect second note hit time");
Assert.AreEqual(2000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[4].Time, "Incorrect second note release time"); Assert.AreEqual(2000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time");
Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1), "Key1 has not been pressed"); Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1), "Key1 has not been released"); Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
Assert.IsTrue(checkContains(generated.Frames[3], ManiaAction.Key2), "Key2 has not been pressed"); Assert.IsTrue(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[4], ManiaAction.Key2), "Key2 has not been released"); Assert.IsFalse(checkContains(generated.Frames[frame_offset + 3], ManiaAction.Key2), "Key2 has not been released");
} }
[Test] [Test]
@ -139,16 +146,16 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate(); var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.IsTrue(generated.Frames.Count == 5, "Replay must have 5 frames"); Assert.IsTrue(generated.Frames.Count == frame_offset + 4, "Replay must have 4 generated frames");
Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect first note hit time"); Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[3].Time, "Incorrect first note release time"); Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 2].Time, "Incorrect first note release time");
Assert.AreEqual(2000, generated.Frames[2].Time, "Incorrect second note hit time"); Assert.AreEqual(2000, generated.Frames[frame_offset + 1].Time, "Incorrect second note hit time");
Assert.AreEqual(4000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[4].Time, "Incorrect second note release time"); Assert.AreEqual(4000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time");
Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1), "Key1 has not been pressed"); Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
Assert.IsTrue(checkContains(generated.Frames[2], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed"); Assert.IsTrue(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
Assert.IsFalse(checkContains(generated.Frames[3], ManiaAction.Key1), "Key1 has not been released"); Assert.IsFalse(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key1), "Key1 has not been released");
Assert.IsTrue(checkContains(generated.Frames[3], ManiaAction.Key2), "Key2 has been released"); Assert.IsTrue(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has been released");
Assert.IsFalse(checkContains(generated.Frames[4], ManiaAction.Key2), "Key2 has not been released"); Assert.IsFalse(checkContains(generated.Frames[frame_offset + 3], ManiaAction.Key2), "Key2 has not been released");
} }
[Test] [Test]
@ -166,14 +173,14 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate(); var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.IsTrue(generated.Frames.Count == 4, "Replay must have 4 frames"); Assert.IsTrue(generated.Frames.Count == frame_offset + 3, "Replay must have 3 generated frames");
Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect first note hit time"); Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
Assert.AreEqual(3000, generated.Frames[2].Time, "Incorrect second note press time + first note release time"); Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect second note press time + first note release time");
Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[3].Time, "Incorrect second note release time"); Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 2].Time, "Incorrect second note release time");
Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1), "Key1 has not been pressed"); Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1), "Key1 has not been released"); Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
Assert.IsTrue(checkContains(generated.Frames[2], ManiaAction.Key2), "Key2 has not been pressed"); Assert.IsTrue(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key2), "Key2 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[3], ManiaAction.Key2), "Key2 has not been released"); Assert.IsFalse(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has not been released");
} }
private bool checkContains(ReplayFrame frame, params ManiaAction[] actions) => actions.All(action => ((ManiaReplayFrame)frame).Actions.Contains(action)); private bool checkContains(ReplayFrame frame, params ManiaAction[] actions) => actions.All(action => ((ManiaReplayFrame)frame).Actions.Contains(action));

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -18,8 +18,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary> /// </summary>
public class DrawableNote : DrawableManiaHitObject<Note>, IKeyBindingHandler<ManiaAction> public class DrawableNote : DrawableManiaHitObject<Note>, IKeyBindingHandler<ManiaAction>
{ {
public const float CORNER_RADIUS = NotePiece.NOTE_HEIGHT / 2;
private readonly NotePiece headPiece; private readonly NotePiece headPiece;
public DrawableNote(Note hitObject) public DrawableNote(Note hitObject)

View File

@ -47,9 +47,6 @@ namespace osu.Game.Rulesets.Mania.Replays
public override Replay Generate() public override Replay Generate()
{ {
// Todo: Realistically this shouldn't be needed, but the first frame is skipped with the way replays are currently handled
Replay.Frames.Add(new ManiaReplayFrame(-100000, 0));
var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time); var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time);
var actions = new List<ManiaAction>(); var actions = new List<ManiaAction>();
@ -70,6 +67,10 @@ namespace osu.Game.Rulesets.Mania.Replays
} }
} }
// todo: can be removed once FramedReplayInputHandler correctly handles rewinding before first frame.
if (Replay.Frames.Count == 0)
Replay.Frames.Add(new ManiaReplayFrame(group.First().Time - 1));
Replay.Frames.Add(new ManiaReplayFrame(group.First().Time, actions.ToArray())); Replay.Frames.Add(new ManiaReplayFrame(group.First().Time, actions.ToArray()));
} }

View File

@ -29,7 +29,8 @@ namespace osu.Game.Rulesets.Osu.Tests
}; };
for (int i = 0; i < 512; i++) for (int i = 0; i < 512; i++)
beatmap.HitObjects.Add(new HitCircle { Position = new Vector2(256, 192), StartTime = i * 100 }); if (i % 32 < 20)
beatmap.HitObjects.Add(new HitCircle { Position = new Vector2(256, 192), StartTime = i * 100 });
return beatmap; return beatmap;
} }

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail), typeof(ModAutoplay) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail), typeof(ModAutoplay) };
public bool AllowFail => false; public bool AllowFail => false;
public bool RestartOnFail => false;
private OsuInputManager inputManager; private OsuInputManager inputManager;

View File

@ -17,7 +17,9 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public override string Description => @"Play with no approach circles and fading circles/sliders."; public override string Description => @"Play with no approach circles and fading circles/sliders.";
public override double ScoreMultiplier => 1.06; public override double ScoreMultiplier => 1.06;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpinIn) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModTraceable), typeof(OsuModSpinIn) };
private const double fade_in_duration_multiplier = 0.4; private const double fade_in_duration_multiplier = 0.4;
private const double fade_out_duration_multiplier = 0.3; private const double fade_out_duration_multiplier = 0.3;

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
// todo: this mod should be able to be compatible with hidden with a bit of further implementation. // todo: this mod should be able to be compatible with hidden with a bit of further implementation.
public override Type[] IncompatibleMods => new[] { typeof(OsuModeObjectScaleTween), typeof(OsuModHidden) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModeObjectScaleTween), typeof(OsuModHidden), typeof(OsuModTraceable) };
private const int rotate_offset = 360; private const int rotate_offset = 360;
private const float rotate_starting_width = 2; private const float rotate_starting_width = 2;

View File

@ -0,0 +1,73 @@
// 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.Linq;
using osu.Framework.Bindables;
using System.Collections.Generic;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods
{
internal class OsuModTraceable : Mod, IReadFromConfig, IApplicableToDrawableHitObjects
{
public override string Name => "Traceable";
public override string Acronym => "TC";
public override IconUsage Icon => FontAwesome.Brands.SnapchatGhost;
public override ModType Type => ModType.Fun;
public override string Description => "Put your faith in the approach circles...";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModHidden), typeof(OsuModSpinIn), typeof(OsuModeObjectScaleTween) };
private Bindable<bool> increaseFirstObjectVisibility = new Bindable<bool>();
public void ReadFromConfig(OsuConfigManager config)
{
increaseFirstObjectVisibility = config.GetBindable<bool>(OsuSetting.IncreaseFirstObjectVisibility);
}
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
{
foreach (var drawable in drawables.Skip(increaseFirstObjectVisibility.Value ? 1 : 0))
drawable.ApplyCustomUpdateState += ApplyTraceableState;
}
protected void ApplyTraceableState(DrawableHitObject drawable, ArmedState state)
{
if (!(drawable is DrawableOsuHitObject drawableOsu))
return;
var h = drawableOsu.HitObject;
switch (drawable)
{
case DrawableHitCircle circle:
// we only want to see the approach circle
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
circle.CirclePiece.Hide();
break;
case DrawableSlider slider:
slider.AccentColour.BindValueChanged(_ =>
{
//will trigger on skin change.
slider.Body.AccentColour = slider.AccentColour.Value.Opacity(0);
slider.Body.BorderColour = slider.AccentColour.Value;
}, true);
break;
case DrawableSpinner spinner:
spinner.Disc.Hide();
spinner.Background.Hide();
break;
}
}
}
}

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Mods
private Bindable<bool> increaseFirstObjectVisibility = new Bindable<bool>(); private Bindable<bool> increaseFirstObjectVisibility = new Bindable<bool>();
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpinIn) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModSpinIn), typeof(OsuModTraceable) };
public void ReadFromConfig(OsuConfigManager config) public void ReadFromConfig(OsuConfigManager config)
{ {

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public class DrawableHitCircle : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach public class DrawableHitCircle : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach
{ {
public ApproachCircle ApproachCircle; public ApproachCircle ApproachCircle { get; }
private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>(); private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>();
private readonly IBindable<int> stackHeightBindable = new Bindable<int>(); private readonly IBindable<int> stackHeightBindable = new Bindable<int>();
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly HitArea hitArea; private readonly HitArea hitArea;
private readonly SkinnableDrawable mainContent; public SkinnableDrawable CirclePiece { get; }
public DrawableHitCircle(HitCircle h) public DrawableHitCircle(HitCircle h)
: base(h) : base(h)
@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return true; return true;
}, },
}, },
mainContent = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.HitCircle), _ => new MainCirclePiece(HitObject.IndexInCurrentCombo)), CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.HitCircle), _ => new MainCirclePiece(HitObject.IndexInCurrentCombo)),
ApproachCircle = new ApproachCircle ApproachCircle = new ApproachCircle
{ {
Alpha = 0, Alpha = 0,
@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
base.UpdateInitialTransforms(); base.UpdateInitialTransforms();
mainContent.FadeInFromZero(HitObject.TimeFadeIn); CirclePiece.FadeInFromZero(HitObject.TimeFadeIn);
ApproachCircle.FadeIn(Math.Min(HitObject.TimeFadeIn * 2, HitObject.TimePreempt)); ApproachCircle.FadeIn(Math.Min(HitObject.TimeFadeIn * 2, HitObject.TimePreempt));
ApproachCircle.ScaleTo(1f, HitObject.TimePreempt); ApproachCircle.ScaleTo(1f, HitObject.TimePreempt);

View File

@ -1,22 +1,66 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Configuration;
using osuTK; using osuTK;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public class DrawableOsuJudgement : DrawableJudgement public class DrawableOsuJudgement : DrawableJudgement
{ {
private SkinnableSprite lighting;
private Bindable<Color4> lightingColour;
public DrawableOsuJudgement(JudgementResult result, DrawableHitObject judgedObject) public DrawableOsuJudgement(JudgementResult result, DrawableHitObject judgedObject)
: base(result, judgedObject) : base(result, judgedObject)
{ {
} }
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
if (config.Get<bool>(OsuSetting.HitLighting) && Result.Type != HitResult.Miss)
{
AddInternal(lighting = new SkinnableSprite("lighting")
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Blending = BlendingParameters.Additive,
Depth = float.MaxValue
});
if (JudgedObject != null)
{
lightingColour = JudgedObject.AccentColour.GetBoundCopy();
lightingColour.BindValueChanged(colour => lighting.Colour = colour.NewValue, true);
}
else
{
lighting.Colour = Color4.White;
}
}
}
protected override double FadeOutDelay => lighting == null ? base.FadeOutDelay : 1400;
protected override void ApplyHitAnimations() protected override void ApplyHitAnimations()
{ {
if (lighting != null)
{
JudgementBody.Delay(FadeInDuration).FadeOut(400);
lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out);
lighting.FadeIn(200).Then().Delay(200).FadeOut(1000);
}
JudgementText?.TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint); JudgementText?.TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
base.ApplyHitAnimations(); base.ApplyHitAnimations();
} }

View File

@ -163,9 +163,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private float sliderPathRadius; private float sliderPathRadius;
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected override void ApplySkin(ISkinSource skin, bool allowFallback)
{ {
base.SkinChanged(skin, allowFallback); base.ApplySkin(skin, allowFallback);
Body.BorderSize = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderBorderSize)?.Value ?? SliderBody.DEFAULT_BORDER_SIZE; Body.BorderSize = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderBorderSize)?.Value ?? SliderBody.DEFAULT_BORDER_SIZE;
sliderPathRadius = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS; sliderPathRadius = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS;
@ -173,6 +173,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Body.AccentColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderTrackOverride)?.Value ?? AccentColour.Value; Body.AccentColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderTrackOverride)?.Value ?? AccentColour.Value;
Body.BorderColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBorder)?.Value ?? Color4.White; Body.BorderColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBorder)?.Value ?? Color4.White;
bool allowBallTint = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false;
Ball.Colour = allowBallTint ? AccentColour.Value : Color4.White;
} }
private void updatePathRadius() => Body.PathRadius = slider.Scale * sliderPathRadius; private void updatePathRadius() => Body.PathRadius = slider.Scale * sliderPathRadius;

View File

@ -139,6 +139,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModSpinIn(), new OsuModSpinIn(),
new MultiMod(new OsuModGrow(), new OsuModDeflate()), new MultiMod(new OsuModGrow(), new OsuModDeflate()),
new MultiMod(new ModWindUp(), new ModWindDown()), new MultiMod(new ModWindUp(), new ModWindDown()),
new OsuModTraceable(),
}; };
case ModType.System: case ModType.System:

View File

@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
HitCircleOverlap, HitCircleOverlap,
SliderBorderSize, SliderBorderSize,
SliderPathRadius, SliderPathRadius,
AllowSliderBallTint,
CursorExpand, CursorExpand,
} }
} }

View File

@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.UI
{ {
Origin = Anchor.Centre, Origin = Anchor.Centre,
Position = ((OsuHitObject)judgedObject.HitObject).StackedEndPosition, Position = ((OsuHitObject)judgedObject.HitObject).StackedEndPosition,
Scale = new Vector2(((OsuHitObject)judgedObject.HitObject).Scale * 1.65f) Scale = new Vector2(((OsuHitObject)judgedObject.HitObject).Scale)
}; };
judgementLayer.Add(explosion); judgementLayer.Add(explosion);

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -15,7 +15,10 @@ using osu.Framework.Logging;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip; using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Writers.Zip;
namespace osu.Game.Tests.Beatmaps.IO namespace osu.Game.Tests.Beatmaps.IO
{ {
@ -87,6 +90,48 @@ namespace osu.Game.Tests.Beatmaps.IO
} }
} }
[Test]
public async Task TestImportCorruptThenImport()
{
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenImport"))
{
try
{
var osu = loadOsu(host);
var imported = await LoadOszIntoOsu(osu);
var firstFile = imported.Files.First();
var files = osu.Dependencies.Get<FileStore>();
long originalLength;
using (var stream = files.Storage.GetStream(firstFile.FileInfo.StoragePath))
originalLength = stream.Length;
using (var stream = files.Storage.GetStream(firstFile.FileInfo.StoragePath, FileAccess.Write, FileMode.Create))
stream.WriteByte(0);
var importedSecondTime = await LoadOszIntoOsu(osu);
using (var stream = files.Storage.GetStream(firstFile.FileInfo.StoragePath))
Assert.AreEqual(stream.Length, originalLength, "Corruption was not fixed on second import");
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
Assert.IsTrue(imported.ID == importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
checkBeatmapSetCount(osu, 1);
checkSingleReferencedFileCount(osu, 18);
}
finally
{
host.Exit();
}
}
}
[Test] [Test]
public async Task TestRollbackOnFailure() public async Task TestRollbackOnFailure()
{ {
@ -135,7 +180,7 @@ namespace osu.Game.Tests.Beatmaps.IO
using (var zip = ZipArchive.Open(brokenOsz)) using (var zip = ZipArchive.Open(brokenOsz))
{ {
zip.AddEntry("broken.osu", brokenOsu, false); zip.AddEntry("broken.osu", brokenOsu, false);
zip.SaveTo(outStream, SharpCompress.Common.CompressionType.Deflate); zip.SaveTo(outStream, CompressionType.Deflate);
} }
// this will trigger purging of the existing beatmap (online set id match) but should rollback due to broken osu. // this will trigger purging of the existing beatmap (online set id match) but should rollback due to broken osu.
@ -366,6 +411,51 @@ namespace osu.Game.Tests.Beatmaps.IO
} }
} }
[Test]
public async Task TestImportNestedStructure()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportNestedStructure"))
{
try
{
var osu = loadOsu(host);
var temp = TestResources.GetTestBeatmapForImport();
string extractedFolder = $"{temp}_extracted";
string subfolder = Path.Combine(extractedFolder, "subfolder");
Directory.CreateDirectory(subfolder);
try
{
using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(subfolder);
using (var zip = ZipArchive.Create())
{
zip.AddAllFromDirectory(extractedFolder);
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
}
var imported = await osu.Dependencies.Get<BeatmapManager>().Import(temp);
ensureLoaded(osu);
Assert.IsFalse(imported.Files.Any(f => f.Filename.Contains("subfolder")), "Files contain common subfolder");
}
finally
{
Directory.Delete(extractedFolder, true);
}
}
finally
{
host.Exit();
}
}
}
public static async Task<BeatmapSetInfo> LoadOszIntoOsu(OsuGameBase osu, string path = null) public static async Task<BeatmapSetInfo> LoadOszIntoOsu(OsuGameBase osu, string path = null)
{ {
var temp = path ?? TestResources.GetTestBeatmapForImport(); var temp = path ?? TestResources.GetTestBeatmapForImport();

View File

@ -0,0 +1,201 @@
// 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;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel;
namespace osu.Game.Tests.NonVisual.Filtering
{
[TestFixture]
public class FilterMatchingTest
{
private BeatmapInfo getExampleBeatmap() => new BeatmapInfo
{
Ruleset = new RulesetInfo { ID = 5 },
StarDifficulty = 4.0d,
BaseDifficulty = new BeatmapDifficulty
{
ApproachRate = 5.0f,
DrainRate = 3.0f,
CircleSize = 2.0f,
},
Metadata = new BeatmapMetadata
{
Artist = "The Artist",
ArtistUnicode = "check unicode too",
Title = "Title goes here",
TitleUnicode = "Title goes here",
AuthorString = "The Author",
Source = "unit tests",
Tags = "look for tags too",
},
Version = "version as well",
Length = 2500,
BPM = 160,
BeatDivisor = 12,
Status = BeatmapSetOnlineStatus.Loved
};
[Test]
public void TestCriteriaMatchingNoRuleset()
{
var exampleBeatmapInfo = getExampleBeatmap();
var criteria = new FilterCriteria();
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
carouselItem.Filter(criteria);
Assert.IsFalse(carouselItem.Filtered.Value);
}
[Test]
public void TestCriteriaMatchingSpecificRuleset()
{
var exampleBeatmapInfo = getExampleBeatmap();
var criteria = new FilterCriteria
{
Ruleset = new RulesetInfo { ID = 6 }
};
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
carouselItem.Filter(criteria);
Assert.IsTrue(carouselItem.Filtered.Value);
}
[Test]
public void TestCriteriaMatchingConvertedBeatmaps()
{
var exampleBeatmapInfo = getExampleBeatmap();
var criteria = new FilterCriteria
{
Ruleset = new RulesetInfo { ID = 6 },
AllowConvertedBeatmaps = true
};
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
carouselItem.Filter(criteria);
Assert.IsFalse(carouselItem.Filtered.Value);
}
[Test]
[TestCase(true)]
[TestCase(false)]
public void TestCriteriaMatchingRangeMin(bool inclusive)
{
var exampleBeatmapInfo = getExampleBeatmap();
var criteria = new FilterCriteria
{
Ruleset = new RulesetInfo { ID = 6 },
AllowConvertedBeatmaps = true,
ApproachRate = new FilterCriteria.OptionalRange<float>
{
IsLowerInclusive = inclusive,
Min = 5.0f
}
};
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
carouselItem.Filter(criteria);
Assert.AreEqual(!inclusive, carouselItem.Filtered.Value);
}
[Test]
[TestCase(true)]
[TestCase(false)]
public void TestCriteriaMatchingRangeMax(bool inclusive)
{
var exampleBeatmapInfo = getExampleBeatmap();
var criteria = new FilterCriteria
{
Ruleset = new RulesetInfo { ID = 6 },
AllowConvertedBeatmaps = true,
BPM = new FilterCriteria.OptionalRange<double>
{
IsUpperInclusive = inclusive,
Max = 160d
}
};
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
carouselItem.Filter(criteria);
Assert.AreEqual(!inclusive, carouselItem.Filtered.Value);
}
[Test]
[TestCase("artist", false)]
[TestCase("artist title author", false)]
[TestCase("an artist", true)]
[TestCase("tags too", false)]
[TestCase("version", false)]
[TestCase("an auteur", true)]
public void TestCriteriaMatchingTerms(string terms, bool filtered)
{
var exampleBeatmapInfo = getExampleBeatmap();
var criteria = new FilterCriteria
{
Ruleset = new RulesetInfo { ID = 6 },
AllowConvertedBeatmaps = true,
SearchText = terms
};
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
carouselItem.Filter(criteria);
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
}
[Test]
[TestCase("", false)]
[TestCase("The", false)]
[TestCase("THE", false)]
[TestCase("author", false)]
[TestCase("the author", false)]
[TestCase("the author AND then something else", true)]
[TestCase("unknown", true)]
public void TestCriteriaMatchingCreator(string creatorName, bool filtered)
{
var exampleBeatmapInfo = getExampleBeatmap();
var criteria = new FilterCriteria
{
Creator = new FilterCriteria.OptionalTextFilter { SearchTerm = creatorName }
};
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
carouselItem.Filter(criteria);
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
}
[Test]
[TestCase("", false)]
[TestCase("The", false)]
[TestCase("THE", false)]
[TestCase("artist", false)]
[TestCase("the artist", false)]
[TestCase("the artist AND then something else", true)]
[TestCase("unicode too", false)]
[TestCase("unknown", true)]
public void TestCriteriaMatchingArtist(string artistName, bool filtered)
{
var exampleBeatmapInfo = getExampleBeatmap();
var criteria = new FilterCriteria
{
Artist = new FilterCriteria.OptionalTextFilter { SearchTerm = artistName }
};
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
carouselItem.Filter(criteria);
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
}
[Test]
[TestCase("", false)]
[TestCase("artist", false)]
[TestCase("unknown", true)]
public void TestCriteriaMatchingArtistWithNullUnicodeName(string artistName, bool filtered)
{
var exampleBeatmapInfo = getExampleBeatmap();
exampleBeatmapInfo.Metadata.ArtistUnicode = null;
var criteria = new FilterCriteria
{
Artist = new FilterCriteria.OptionalTextFilter { SearchTerm = artistName }
};
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
carouselItem.Filter(criteria);
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
}
}
}

View File

@ -0,0 +1,184 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select;
namespace osu.Game.Tests.NonVisual.Filtering
{
[TestFixture]
public class FilterQueryParserTest
{
[Test]
public void TestApplyQueriesBareWords()
{
const string query = "looking for a beatmap";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("looking for a beatmap", filterCriteria.SearchText);
Assert.AreEqual(4, filterCriteria.SearchTerms.Length);
}
/*
* The following tests have been written a bit strangely (they don't check exact
* bound equality with what the filter says).
* This is to account for floating-point arithmetic issues.
* For example, specifying a bpm<140 filter would previously match beatmaps with BPM
* of 139.99999, which would be displayed in the UI as 140.
* Due to this the tests check the last tick inside the range and the first tick
* outside of the range.
*/
[Test]
public void TestApplyStarQueries()
{
const string query = "stars<4 easy";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("easy", filterCriteria.SearchText.Trim());
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
Assert.IsNotNull(filterCriteria.StarDifficulty.Max);
Assert.Greater(filterCriteria.StarDifficulty.Max, 3.99d);
Assert.Less(filterCriteria.StarDifficulty.Max, 4.00d);
Assert.IsNull(filterCriteria.StarDifficulty.Min);
}
[Test]
public void TestApplyApproachRateQueries()
{
const string query = "ar>=9 difficult";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("difficult", filterCriteria.SearchText.Trim());
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
Assert.IsNotNull(filterCriteria.ApproachRate.Min);
Assert.Greater(filterCriteria.ApproachRate.Min, 8.9f);
Assert.Less(filterCriteria.ApproachRate.Min, 9.0f);
Assert.IsNull(filterCriteria.ApproachRate.Max);
}
[Test]
public void TestApplyDrainRateQueries()
{
const string query = "dr>2 quite specific dr<:6";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("quite specific", filterCriteria.SearchText.Trim());
Assert.AreEqual(2, filterCriteria.SearchTerms.Length);
Assert.Greater(filterCriteria.DrainRate.Min, 2.0f);
Assert.Less(filterCriteria.DrainRate.Min, 2.1f);
Assert.Greater(filterCriteria.DrainRate.Max, 6.0f);
Assert.Less(filterCriteria.DrainRate.Min, 6.1f);
}
[Test]
public void TestApplyBPMQueries()
{
const string query = "bpm>:200 gotta go fast";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("gotta go fast", filterCriteria.SearchText.Trim());
Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
Assert.IsNotNull(filterCriteria.BPM.Min);
Assert.Greater(filterCriteria.BPM.Min, 199.99d);
Assert.Less(filterCriteria.BPM.Min, 200.00d);
Assert.IsNull(filterCriteria.BPM.Max);
}
private static object[] lengthQueryExamples =
{
new object[] { "6ms", TimeSpan.FromMilliseconds(6), TimeSpan.FromMilliseconds(1) },
new object[] { "23s", TimeSpan.FromSeconds(23), TimeSpan.FromSeconds(1) },
new object[] { "9m", TimeSpan.FromMinutes(9), TimeSpan.FromMinutes(1) },
new object[] { "0.25h", TimeSpan.FromHours(0.25), TimeSpan.FromHours(1) },
new object[] { "70", TimeSpan.FromSeconds(70), TimeSpan.FromSeconds(1) },
};
[Test]
[TestCaseSource(nameof(lengthQueryExamples))]
public void TestApplyLengthQueries(string lengthQuery, TimeSpan expectedLength, TimeSpan scale)
{
string query = $"length={lengthQuery} time";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("time", filterCriteria.SearchText.Trim());
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
Assert.AreEqual(expectedLength.TotalMilliseconds - scale.TotalMilliseconds / 2.0, filterCriteria.Length.Min);
Assert.AreEqual(expectedLength.TotalMilliseconds + scale.TotalMilliseconds / 2.0, filterCriteria.Length.Max);
}
[Test]
public void TestApplyDivisorQueries()
{
const string query = "that's a time signature alright! divisor:12";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("that's a time signature alright!", filterCriteria.SearchText.Trim());
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
Assert.AreEqual(12, filterCriteria.BeatDivisor.Min);
Assert.IsTrue(filterCriteria.BeatDivisor.IsLowerInclusive);
Assert.AreEqual(12, filterCriteria.BeatDivisor.Max);
Assert.IsTrue(filterCriteria.BeatDivisor.IsUpperInclusive);
}
[Test]
public void TestApplyStatusQueries()
{
const string query = "I want the pp status=ranked";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("I want the pp", filterCriteria.SearchText.Trim());
Assert.AreEqual(4, filterCriteria.SearchTerms.Length);
Assert.AreEqual(BeatmapSetOnlineStatus.Ranked, filterCriteria.OnlineStatus.Min);
Assert.IsTrue(filterCriteria.OnlineStatus.IsLowerInclusive);
Assert.AreEqual(BeatmapSetOnlineStatus.Ranked, filterCriteria.OnlineStatus.Max);
Assert.IsTrue(filterCriteria.OnlineStatus.IsUpperInclusive);
}
[Test]
public void TestApplyCreatorQueries()
{
const string query = "beatmap specifically by creator=my_fav";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("beatmap specifically by", filterCriteria.SearchText.Trim());
Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
Assert.AreEqual("my_fav", filterCriteria.Creator.SearchTerm);
}
[Test]
public void TestApplyArtistQueries()
{
const string query = "find me songs by artist=singer please";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("find me songs by please", filterCriteria.SearchText.Trim());
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
Assert.AreEqual("singer", filterCriteria.Artist.SearchTerm);
}
[Test]
public void TestApplyArtistQueriesWithSpaces()
{
const string query = "really like artist=\"name with space\" yes";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("really like yes", filterCriteria.SearchText.Trim());
Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
Assert.AreEqual("name with space", filterCriteria.Artist.SearchTerm);
}
[Test]
public void TestApplyArtistQueriesOneDoubleQuote()
{
const string query = "weird artist=double\"quote";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("weird", filterCriteria.SearchText.Trim());
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
Assert.AreEqual("double\"quote", filterCriteria.Artist.SearchTerm);
}
}
}

View File

@ -117,17 +117,57 @@ namespace osu.Game.Tests.Scores.IO
} }
} }
[Test]
public async Task TestImportWithDeletedBeatmapSet()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWithDeletedBeatmapSet"))
{
try
{
var osu = await loadOsu(host);
var toImport = new ScoreInfo
{
Hash = Guid.NewGuid().ToString(),
Statistics = new Dictionary<HitResult, int>
{
{ HitResult.Perfect, 100 },
{ HitResult.Miss, 50 }
}
};
var imported = await loadIntoOsu(osu, toImport);
var beatmapManager = osu.Dependencies.Get<BeatmapManager>();
var scoreManager = osu.Dependencies.Get<ScoreManager>();
beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.Beatmap.ID)));
Assert.That(scoreManager.Query(s => s.ID == imported.ID).DeletePending, Is.EqualTo(true));
var secondImport = await loadIntoOsu(osu, imported);
Assert.That(secondImport, Is.Null);
}
finally
{
host.Exit();
}
}
}
private async Task<ScoreInfo> loadIntoOsu(OsuGameBase osu, ScoreInfo score) private async Task<ScoreInfo> loadIntoOsu(OsuGameBase osu, ScoreInfo score)
{ {
var beatmapManager = osu.Dependencies.Get<BeatmapManager>(); var beatmapManager = osu.Dependencies.Get<BeatmapManager>();
score.Beatmap = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); if (score.Beatmap == null)
score.Ruleset = new OsuRuleset().RulesetInfo; score.Beatmap = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
if (score.Ruleset == null)
score.Ruleset = new OsuRuleset().RulesetInfo;
var scoreManager = osu.Dependencies.Get<ScoreManager>(); var scoreManager = osu.Dependencies.Get<ScoreManager>();
await scoreManager.Import(score); await scoreManager.Import(score);
return scoreManager.GetAllUsableScores().First(); return scoreManager.GetAllUsableScores().FirstOrDefault();
} }
private async Task<OsuGameBase> loadOsu(GameHost host) private async Task<OsuGameBase> loadOsu(GameHost host)

View File

@ -3,6 +3,7 @@
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -12,6 +13,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Description("Player instantiated with an autoplay mod.")] [Description("Player instantiated with an autoplay mod.")]
public class TestSceneAutoplay : AllPlayersTestScene public class TestSceneAutoplay : AllPlayersTestScene
{ {
private ClockBackedTestWorkingBeatmap.TrackVirtualManual track;
protected override Player CreatePlayer(Ruleset ruleset) protected override Player CreatePlayer(Ruleset ruleset)
{ {
Mods.Value = Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray(); Mods.Value = Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray();
@ -21,7 +24,18 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void AddCheckSteps() protected override void AddCheckSteps()
{ {
AddUntilStep("score above zero", () => ((ScoreAccessiblePlayer)Player).ScoreProcessor.TotalScore.Value > 0); AddUntilStep("score above zero", () => ((ScoreAccessiblePlayer)Player).ScoreProcessor.TotalScore.Value > 0);
AddUntilStep("key counter counted keys", () => ((ScoreAccessiblePlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 0)); AddUntilStep("key counter counted keys", () => ((ScoreAccessiblePlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2));
AddStep("rewind", () => track.Seek(-10000));
AddUntilStep("key counter reset", () => ((ScoreAccessiblePlayer)Player).HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0));
}
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap)
{
var working = base.CreateWorkingBeatmap(beatmap);
track = (ClockBackedTestWorkingBeatmap.TrackVirtualManual)working.Track;
return working;
} }
private class ScoreAccessiblePlayer : TestPlayer private class ScoreAccessiblePlayer : TestPlayer
@ -29,6 +43,8 @@ namespace osu.Game.Tests.Visual.Gameplay
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
public new HUDOverlay HUDOverlay => base.HUDOverlay; public new HUDOverlay HUDOverlay => base.HUDOverlay;
public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer;
public ScoreAccessiblePlayer() public ScoreAccessiblePlayer()
: base(false, false) : base(false, false)
{ {

View File

@ -6,8 +6,6 @@ using System.Linq;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
@ -17,9 +15,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override Player CreatePlayer(Ruleset ruleset) protected override Player CreatePlayer(Ruleset ruleset)
{ {
Mods.Value = Array.Empty<Mod>(); Mods.Value = Array.Empty<Mod>();
return new FailPlayer();
var beatmap = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Array.Empty<Mod>());
return new FailPlayer(ruleset.GetAutoplayMod().CreateReplayScore(beatmap));
} }
protected override void AddCheckSteps() protected override void AddCheckSteps()
@ -29,16 +25,12 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("total judgements == 1", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits == 1); AddAssert("total judgements == 1", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits == 1);
} }
private class FailPlayer : ReplayPlayer private class FailPlayer : TestPlayer
{ {
public new DrawableRuleset DrawableRuleset => base.DrawableRuleset;
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
protected override bool PauseOnFocusLost => false; public FailPlayer()
: base(false, false)
public FailPlayer(Score score)
: base(score, false, false)
{ {
} }

View File

@ -14,6 +14,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osuTK; using osuTK;
@ -47,9 +48,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for track to start running", () => track.IsRunning); AddUntilStep("wait for track to start running", () => track.IsRunning);
addSeekStep(3000); addSeekStep(3000);
AddAssert("all judged", () => player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged)); AddAssert("all judged", () => player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged));
AddUntilStep("key counter counted keys", () => player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses >= 7));
AddStep("clear results", () => player.AppliedResults.Clear()); AddStep("clear results", () => player.AppliedResults.Clear());
addSeekStep(0); addSeekStep(0);
AddAssert("none judged", () => player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged)); AddAssert("none judged", () => player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged));
AddUntilStep("key counters reset", () => player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0));
AddAssert("no results triggered", () => player.AppliedResults.Count == 0); AddAssert("no results triggered", () => player.AppliedResults.Count == 0);
} }
@ -90,6 +93,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public readonly List<JudgementResult> AppliedResults = new List<JudgementResult>(); public readonly List<JudgementResult> AppliedResults = new List<JudgementResult>();
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
public new HUDOverlay HUDOverlay => base.HUDOverlay;
public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer;
public new DrawableRuleset DrawableRuleset => base.DrawableRuleset; public new DrawableRuleset DrawableRuleset => base.DrawableRuleset;

View File

@ -7,7 +7,6 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.MathUtils; using osu.Framework.MathUtils;
using osu.Framework.Timing;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osuTK.Input; using osuTK.Input;
@ -25,14 +24,15 @@ namespace osu.Game.Tests.Visual.Gameplay
public TestSceneKeyCounter() public TestSceneKeyCounter()
{ {
KeyCounterKeyboard rewindTestKeyCounterKeyboard; KeyCounterKeyboard testCounter;
KeyCounterDisplay kc = new KeyCounterDisplay KeyCounterDisplay kc = new KeyCounterDisplay
{ {
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Children = new KeyCounter[] Children = new KeyCounter[]
{ {
rewindTestKeyCounterKeyboard = new KeyCounterKeyboard(Key.X), testCounter = new KeyCounterKeyboard(Key.X),
new KeyCounterKeyboard(Key.X), new KeyCounterKeyboard(Key.X),
new KeyCounterMouse(MouseButton.Left), new KeyCounterMouse(MouseButton.Left),
new KeyCounterMouse(MouseButton.Right), new KeyCounterMouse(MouseButton.Right),
@ -44,10 +44,8 @@ namespace osu.Game.Tests.Visual.Gameplay
Key key = (Key)((int)Key.A + RNG.Next(26)); Key key = (Key)((int)Key.A + RNG.Next(26));
kc.Add(new KeyCounterKeyboard(key)); kc.Add(new KeyCounterKeyboard(key));
}); });
AddSliderStep("Fade time", 0, 200, 50, v => kc.FadeTime = v);
Key testKey = ((KeyCounterKeyboard)kc.Children.First()).Key; Key testKey = ((KeyCounterKeyboard)kc.Children.First()).Key;
double time1 = 0;
AddStep($"Press {testKey} key", () => AddStep($"Press {testKey} key", () =>
{ {
@ -55,48 +53,17 @@ namespace osu.Game.Tests.Visual.Gameplay
InputManager.ReleaseKey(testKey); InputManager.ReleaseKey(testKey);
}); });
AddAssert($"Check {testKey} counter after keypress", () => rewindTestKeyCounterKeyboard.CountPresses == 1); AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 1);
AddStep($"Press {testKey} key", () => AddStep($"Press {testKey} key", () =>
{ {
InputManager.PressKey(testKey); InputManager.PressKey(testKey);
InputManager.ReleaseKey(testKey); InputManager.ReleaseKey(testKey);
time1 = Clock.CurrentTime;
}); });
AddAssert($"Check {testKey} counter after keypress", () => rewindTestKeyCounterKeyboard.CountPresses == 2); AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 2);
IFrameBasedClock oldClock = null;
AddStep($"Rewind {testKey} counter once", () =>
{
oldClock = rewindTestKeyCounterKeyboard.Clock;
rewindTestKeyCounterKeyboard.Clock = new FramedOffsetClock(new FixedClock(time1 - 10));
});
AddAssert($"Check {testKey} counter after rewind", () => rewindTestKeyCounterKeyboard.CountPresses == 1);
AddStep($"Rewind {testKey} counter to zero", () => rewindTestKeyCounterKeyboard.Clock = new FramedOffsetClock(new FixedClock(0)));
AddAssert($"Check {testKey} counter after rewind", () => rewindTestKeyCounterKeyboard.CountPresses == 0);
AddStep("Restore clock", () => rewindTestKeyCounterKeyboard.Clock = oldClock);
Add(kc); Add(kc);
} }
private class FixedClock : IClock
{
private readonly double time;
public FixedClock(double time)
{
this.time = time;
}
public double CurrentTime => time;
public double Rate => 1;
public bool IsRunning => false;
}
} }
} }

View File

@ -26,12 +26,14 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
AddUntilStep("score above zero", () => ((ScoreAccessibleReplayPlayer)Player).ScoreProcessor.TotalScore.Value > 0); AddUntilStep("score above zero", () => ((ScoreAccessibleReplayPlayer)Player).ScoreProcessor.TotalScore.Value > 0);
AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 0)); AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 0));
AddAssert("cannot fail", () => !((ScoreAccessibleReplayPlayer)Player).AllowFail);
} }
private class ScoreAccessibleReplayPlayer : ReplayPlayer private class ScoreAccessibleReplayPlayer : ReplayPlayer
{ {
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
public new HUDOverlay HUDOverlay => base.HUDOverlay; public new HUDOverlay HUDOverlay => base.HUDOverlay;
public new bool AllowFail => base.AllowFail;
protected override bool PauseOnFocusLost => false; protected override bool PauseOnFocusLost => false;

View File

@ -68,6 +68,34 @@ namespace osu.Game.Tests.Visual.Online
changelog.ShowListing(); changelog.ShowListing();
changelog.Show(); changelog.Show();
}); });
AddStep(@"Ensure HTML string unescaping", () =>
{
changelog.ShowBuild(new APIChangelogBuild
{
Version = "2019.920.0",
DisplayVersion = "2019.920.0",
UpdateStream = new APIUpdateStream
{
Name = "Test",
DisplayName = "Test"
},
ChangelogEntries = new List<APIChangelogEntry>
{
new APIChangelogEntry
{
Category = "Testing HTML strings unescaping",
Title = "Ensuring HTML strings are being unescaped",
MessageHtml = "&quot;&quot;&quot;This text should appear triple-quoted&quot;&quot;&quot; &gt;_&lt;",
GithubUser = new APIChangelogUser
{
DisplayName = "Dummy",
OsuUsername = "Dummy",
}
},
}
});
});
} }
} }
} }

View File

@ -242,6 +242,21 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false)); AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
AddAssert("Selection is non-null", () => currentSelection != null); AddAssert("Selection is non-null", () => currentSelection != null);
setSelected(1, 3);
AddStep("Apply a range filter", () => carousel.Filter(new FilterCriteria
{
SearchText = "#3",
StarDifficulty = new FilterCriteria.OptionalRange<double>
{
Min = 2,
Max = 5.5,
IsLowerInclusive = true
}
}, false));
checkSelected(3, 2);
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
} }
/// <summary> /// <summary>

View File

@ -3,10 +3,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards; using osu.Game.Online.Leaderboards;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Users; using osu.Game.Users;
@ -14,19 +16,20 @@ using osuTK;
namespace osu.Game.Tests.Visual.SongSelect namespace osu.Game.Tests.Visual.SongSelect
{ {
[Description("PlaySongSelect leaderboard")] public class TestSceneBeatmapLeaderboard : OsuTestScene
public class TestSceneLeaderboard : OsuTestScene
{ {
public override IReadOnlyList<Type> RequiredTypes => new[] public override IReadOnlyList<Type> RequiredTypes => new[]
{ {
typeof(Placeholder), typeof(Placeholder),
typeof(MessagePlaceholder), typeof(MessagePlaceholder),
typeof(RetrievalFailurePlaceholder), typeof(RetrievalFailurePlaceholder),
typeof(UserTopScoreContainer),
typeof(Leaderboard<BeatmapLeaderboardScope, ScoreInfo>),
}; };
private readonly FailableLeaderboard leaderboard; private readonly FailableLeaderboard leaderboard;
public TestSceneLeaderboard() public TestSceneBeatmapLeaderboard()
{ {
Add(leaderboard = new FailableLeaderboard Add(leaderboard = new FailableLeaderboard
{ {
@ -37,6 +40,7 @@ namespace osu.Game.Tests.Visual.SongSelect
}); });
AddStep(@"New Scores", newScores); AddStep(@"New Scores", newScores);
AddStep(@"Show personal best", showPersonalBest);
AddStep(@"Empty Scores", () => leaderboard.SetRetrievalState(PlaceholderState.NoScores)); AddStep(@"Empty Scores", () => leaderboard.SetRetrievalState(PlaceholderState.NoScores));
AddStep(@"Network failure", () => leaderboard.SetRetrievalState(PlaceholderState.NetworkFailure)); AddStep(@"Network failure", () => leaderboard.SetRetrievalState(PlaceholderState.NetworkFailure));
AddStep(@"No supporter", () => leaderboard.SetRetrievalState(PlaceholderState.NotSupporter)); AddStep(@"No supporter", () => leaderboard.SetRetrievalState(PlaceholderState.NotSupporter));
@ -47,6 +51,32 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep($"{status} beatmap", () => showBeatmapWithStatus(status)); AddStep($"{status} beatmap", () => showBeatmapWithStatus(status));
} }
private void showPersonalBest()
{
leaderboard.TopScore = new APILegacyUserTopScoreInfo
{
Position = 999,
Score = new APILegacyScoreInfo
{
Rank = ScoreRank.XH,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
User = new User
{
Id = 6602580,
Username = @"waaiiru",
Country = new Country
{
FullName = @"Spain",
FlagName = @"ES",
},
},
}
};
}
private void newScores() private void newScores()
{ {
var scores = new[] var scores = new[]

View File

@ -0,0 +1,119 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osuTK.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.SongSelect
{
public class TestSceneUserTopScoreContainer : OsuTestScene
{
public TestSceneUserTopScoreContainer()
{
UserTopScoreContainer topScoreContainer;
Add(new Container
{
Origin = Anchor.BottomCentre,
Anchor = Anchor.Centre,
AutoSizeAxes = Axes.Y,
Width = 500,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.DarkGreen,
},
topScoreContainer = new UserTopScoreContainer
{
Origin = Anchor.BottomCentre,
Anchor = Anchor.BottomCentre,
}
}
});
var scores = new[]
{
new APILegacyUserTopScoreInfo
{
Position = 999,
Score = new APILegacyScoreInfo
{
Rank = ScoreRank.XH,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
User = new User
{
Id = 6602580,
Username = @"waaiiru",
Country = new Country
{
FullName = @"Spain",
FlagName = @"ES",
},
},
}
},
new APILegacyUserTopScoreInfo
{
Position = 110000,
Score = new APILegacyScoreInfo
{
Rank = ScoreRank.X,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
User = new User
{
Id = 4608074,
Username = @"Skycries",
Country = new Country
{
FullName = @"Brazil",
FlagName = @"BR",
},
},
}
},
new APILegacyUserTopScoreInfo
{
Position = 22333,
Score = new APILegacyScoreInfo
{
Rank = ScoreRank.S,
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
User = new User
{
Id = 1541390,
Username = @"Toukai",
Country = new Country
{
FullName = @"Canada",
FlagName = @"CA",
},
},
}
}
};
AddStep(@"Trigger visibility", topScoreContainer.ToggleVisibility);
AddStep(@"Add score(rank 999)", () => topScoreContainer.Score.Value = scores[0]);
AddStep(@"Add score(rank 110000)", () => topScoreContainer.Score.Value = scores[1]);
AddStep(@"Add score(rank 22333)", () => topScoreContainer.Score.Value = scores[2]);
AddStep(@"Add null score", () => topScoreContainer.Score.Value = null);
}
}
}

View File

@ -52,19 +52,23 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("start confirming", () => overlay.Begin()); AddStep("start confirming", () => overlay.Begin());
AddStep("abort confirming", () => overlay.Abort()); AddStep("abort confirming", () => overlay.Abort());
AddAssert("ensure not fired internally", () => !overlay.Fired);
AddAssert("ensure aborted", () => !fired); AddAssert("ensure aborted", () => !fired);
AddStep("start confirming", () => overlay.Begin()); AddStep("start confirming", () => overlay.Begin());
AddUntilStep("wait until confirmed", () => fired); AddUntilStep("wait until confirmed", () => fired);
AddAssert("ensure fired internally", () => overlay.Fired);
AddStep("abort after fire", () => overlay.Abort());
AddAssert("ensure not fired internally", () => !overlay.Fired);
AddStep("start confirming", () => overlay.Begin());
AddUntilStep("wait until fired again", () => overlay.Fired);
} }
private class TestHoldToConfirmOverlay : ExitConfirmOverlay private class TestHoldToConfirmOverlay : ExitConfirmOverlay
{ {
protected override bool AllowMultipleFires => true;
public void Begin() => BeginConfirm(); public void Begin() => BeginConfirm();
public void Abort() => AbortConfirm();
} }
} }
} }

View File

@ -0,0 +1,89 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Sprites;
using osu.Game.Screens.Edit.Setup.Components.LabelledComponents;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneLabelledComponent : OsuTestScene
{
[TestCase(false)]
[TestCase(true)]
public void TestPadded(bool hasDescription) => createPaddedComponent(hasDescription);
[TestCase(false)]
[TestCase(true)]
public void TestNonPadded(bool hasDescription) => createPaddedComponent(hasDescription, false);
private void createPaddedComponent(bool hasDescription = false, bool padded = true)
{
AddStep("create component", () =>
{
LabelledComponent component;
Child = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
AutoSizeAxes = Axes.Y,
Child = component = padded ? (LabelledComponent)new PaddedLabelledComponent() : new NonPaddedLabelledComponent(),
};
component.Label = "a sample component";
component.Description = hasDescription ? "this text describes the component" : string.Empty;
});
}
private class PaddedLabelledComponent : LabelledComponent
{
public PaddedLabelledComponent()
: base(true)
{
}
protected override Drawable CreateComponent() => new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.Red,
Text = @"(( Component ))"
};
}
private class NonPaddedLabelledComponent : LabelledComponent
{
public NonPaddedLabelledComponent()
: base(false)
{
}
protected override Drawable CreateComponent() => new Container
{
RelativeSizeAxes = Axes.X,
Height = 40,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.Red,
Text = @"(( Component ))"
}
}
};
}
}
}

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="DeepEqual" Version="2.0.0" /> <PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -5,7 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
</ItemGroup> </ItemGroup>

View File

@ -11,6 +11,6 @@
<ProjectReference Include="..\osu.Game\osu.Game.csproj" /> <ProjectReference Include="..\osu.Game\osu.Game.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" /> <PackageReference Include="Microsoft.Win32.Registry" Version="4.6.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -89,7 +89,7 @@ namespace osu.Game.Beatmaps
protected override Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) protected override Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default)
{ {
if (archive != null) if (archive != null)
beatmapSet.Beatmaps = createBeatmapDifficulties(archive); beatmapSet.Beatmaps = createBeatmapDifficulties(beatmapSet.Files);
foreach (BeatmapInfo b in beatmapSet.Beatmaps) foreach (BeatmapInfo b in beatmapSet.Beatmaps)
{ {
@ -279,13 +279,13 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// Create all required <see cref="BeatmapInfo"/>s for the provided archive. /// Create all required <see cref="BeatmapInfo"/>s for the provided archive.
/// </summary> /// </summary>
private List<BeatmapInfo> createBeatmapDifficulties(ArchiveReader reader) private List<BeatmapInfo> createBeatmapDifficulties(List<BeatmapSetFileInfo> files)
{ {
var beatmapInfos = new List<BeatmapInfo>(); var beatmapInfos = new List<BeatmapInfo>();
foreach (var name in reader.Filenames.Where(f => f.EndsWith(".osu"))) foreach (var file in files.Where(f => f.Filename.EndsWith(".osu")))
{ {
using (var raw = reader.GetStream(name)) using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath))
using (var ms = new MemoryStream()) //we need a memory stream so we can seek using (var ms = new MemoryStream()) //we need a memory stream so we can seek
using (var sr = new StreamReader(ms)) using (var sr = new StreamReader(ms))
{ {
@ -295,12 +295,13 @@ namespace osu.Game.Beatmaps
var decoder = Decoder.GetDecoder<Beatmap>(sr); var decoder = Decoder.GetDecoder<Beatmap>(sr);
IBeatmap beatmap = decoder.Decode(sr); IBeatmap beatmap = decoder.Decode(sr);
beatmap.BeatmapInfo.Path = name; beatmap.BeatmapInfo.Path = file.Filename;
beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash(); beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash();
beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash(); beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash();
var ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID); var ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID);
beatmap.BeatmapInfo.Ruleset = ruleset; beatmap.BeatmapInfo.Ruleset = ruleset;
// TODO: this should be done in a better place once we actually need to dynamically update it. // TODO: this should be done in a better place once we actually need to dynamically update it.
beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0; beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0;
beatmap.BeatmapInfo.Length = calculateLength(beatmap); beatmap.BeatmapInfo.Length = calculateLength(beatmap);

View File

@ -81,6 +81,8 @@ namespace osu.Game.Configuration
Set(OsuSetting.DimLevel, 0.3, 0, 1, 0.01); Set(OsuSetting.DimLevel, 0.3, 0, 1, 0.01);
Set(OsuSetting.BlurLevel, 0, 0, 1, 0.01); Set(OsuSetting.BlurLevel, 0, 0, 1, 0.01);
Set(OsuSetting.HitLighting, true);
Set(OsuSetting.ShowInterface, true); Set(OsuSetting.ShowInterface, true);
Set(OsuSetting.ShowHealthDisplayWhenCantFail, true); Set(OsuSetting.ShowHealthDisplayWhenCantFail, true);
Set(OsuSetting.KeyOverlay, false); Set(OsuSetting.KeyOverlay, false);
@ -112,6 +114,8 @@ namespace osu.Game.Configuration
Set(OsuSetting.UIScale, 1f, 0.8f, 1.6f, 0.01f); Set(OsuSetting.UIScale, 1f, 0.8f, 1.6f, 0.01f);
Set(OsuSetting.UIHoldActivationDelay, 200, 0, 500);
Set(OsuSetting.IntroSequence, IntroSequence.Triangles); Set(OsuSetting.IntroSequence, IntroSequence.Triangles);
} }
@ -180,6 +184,8 @@ namespace osu.Game.Configuration
ScalingSizeX, ScalingSizeX,
ScalingSizeY, ScalingSizeY,
UIScale, UIScale,
IntroSequence IntroSequence,
UIHoldActivationDelay,
HitLighting
} }
} }

View File

@ -7,10 +7,12 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Humanizer;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using osu.Framework; using osu.Framework;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.IO.File; using osu.Framework.IO.File;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
@ -109,7 +111,7 @@ namespace osu.Game.Database
protected async Task Import(ProgressNotification notification, params string[] paths) protected async Task Import(ProgressNotification notification, params string[] paths)
{ {
notification.Progress = 0; notification.Progress = 0;
notification.Text = "Import is initialising..."; notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising...";
int current = 0; int current = 0;
@ -145,7 +147,7 @@ namespace osu.Game.Database
if (imported.Count == 0) if (imported.Count == 0)
{ {
notification.Text = "Import failed!"; notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import failed!";
notification.State = ProgressNotificationState.Cancelled; notification.State = ProgressNotificationState.Cancelled;
} }
else else
@ -481,12 +483,16 @@ namespace osu.Game.Database
{ {
var fileInfos = new List<TFileModel>(); var fileInfos = new List<TFileModel>();
string prefix = reader.Filenames.GetCommonPrefix();
if (!(prefix.EndsWith("/") || prefix.EndsWith("\\")))
prefix = string.Empty;
// import files to manager // import files to manager
foreach (string file in reader.Filenames) foreach (string file in reader.Filenames)
using (Stream s = reader.GetStream(file)) using (Stream s = reader.GetStream(file))
fileInfos.Add(new TFileModel fileInfos.Add(new TFileModel
{ {
Filename = FileSafety.PathStandardise(file), Filename = FileSafety.PathStandardise(file.Substring(prefix.Length)),
FileInfo = files.Add(s) FileInfo = files.Add(s)
}); });
@ -585,7 +591,7 @@ namespace osu.Game.Database
/// </summary> /// </summary>
/// <param name="existing">The existing model.</param> /// <param name="existing">The existing model.</param>
/// <param name="import">The newly imported model.</param> /// <param name="import">The newly imported model.</param>
/// <returns>Whether the existing model should be restored and used. Returning false will delete the existing a force a re-import.</returns> /// <returns>Whether the existing model should be restored and used. Returning false will delete the existing and force a re-import.</returns>
protected virtual bool CanUndelete(TModel existing, TModel import) => true; protected virtual bool CanUndelete(TModel existing, TModel import) => true;
private DbSet<TModel> queryModel() => ContextFactory.Get().Set<TModel>(); private DbSet<TModel> queryModel() => ContextFactory.Get().Set<TModel>();

View File

@ -2,9 +2,11 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
namespace osu.Game.Graphics.Containers namespace osu.Game.Graphics.Containers
{ {
@ -12,12 +14,13 @@ namespace osu.Game.Graphics.Containers
{ {
public Action Action; public Action Action;
private const int default_activation_delay = 200;
private const int fadeout_delay = 200; private const int fadeout_delay = 200;
private readonly double activationDelay; /// <summary>
/// Whether currently in a fired state (and the confirm <see cref="Action"/> has been sent).
/// </summary>
public bool Fired { get; private set; }
private bool fired;
private bool confirming; private bool confirming;
/// <summary> /// <summary>
@ -27,35 +30,35 @@ namespace osu.Game.Graphics.Containers
public Bindable<double> Progress = new BindableDouble(); public Bindable<double> Progress = new BindableDouble();
/// <summary> private Bindable<int> holdActivationDelay;
/// Create a new instance.
/// </summary> [BackgroundDependencyLoader]
/// <param name="activationDelay">The time requried before an action is confirmed.</param> private void load(OsuConfigManager config)
protected HoldToConfirmContainer(double activationDelay = default_activation_delay)
{ {
this.activationDelay = activationDelay; holdActivationDelay = config.GetBindable<int>(OsuSetting.UIHoldActivationDelay);
} }
protected void BeginConfirm() protected void BeginConfirm()
{ {
if (confirming || (!AllowMultipleFires && fired)) return; if (confirming || (!AllowMultipleFires && Fired)) return;
confirming = true; confirming = true;
this.TransformBindableTo(Progress, 1, activationDelay * (1 - Progress.Value), Easing.Out).OnComplete(_ => Confirm()); this.TransformBindableTo(Progress, 1, holdActivationDelay.Value * (1 - Progress.Value), Easing.Out).OnComplete(_ => Confirm());
} }
protected virtual void Confirm() protected virtual void Confirm()
{ {
Action?.Invoke(); Action?.Invoke();
fired = true; Fired = true;
} }
protected void AbortConfirm() protected void AbortConfirm()
{ {
if (!AllowMultipleFires && fired) return; if (!AllowMultipleFires && Fired) return;
confirming = false; confirming = false;
Fired = false;
this.TransformBindableTo(Progress, 0, fadeout_delay, Easing.Out); this.TransformBindableTo(Progress, 0, fadeout_delay, Easing.Out);
} }

View File

@ -50,7 +50,16 @@ namespace osu.Game.IO
string path = info.StoragePath; string path = info.StoragePath;
// we may be re-adding a file to fix missing store entries. // we may be re-adding a file to fix missing store entries.
if (!Storage.Exists(path)) bool requiresCopy = !Storage.Exists(path);
if (!requiresCopy)
{
// even if the file already exists, check the existing checksum for safety.
using (var stream = Storage.GetStream(path))
requiresCopy |= stream.ComputeSHA2Hash() != hash;
}
if (requiresCopy)
{ {
data.Seek(0, SeekOrigin.Begin); data.Seek(0, SeekOrigin.Begin);

View File

@ -24,7 +24,7 @@ namespace osu.Game.Online.API.Requests.Responses
public string Url { get; set; } public string Url { get; set; }
[JsonProperty("type")] [JsonProperty("type")]
public string Type { get; set; } public ChangelogEntryType Type { get; set; }
[JsonProperty("category")] [JsonProperty("category")]
public string Category { get; set; } public string Category { get; set; }
@ -44,4 +44,10 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty("github_user")] [JsonProperty("github_user")]
public APIChangelogUser GithubUser { get; set; } public APIChangelogUser GithubUser { get; set; }
} }
public enum ChangelogEntryType
{
Add,
Fix
}
} }

View File

@ -35,6 +35,10 @@ namespace osu.Game.Online.Leaderboards
private bool scoresLoadedOnce; private bool scoresLoadedOnce;
private readonly Container content;
protected override Container<Drawable> Content => content;
private IEnumerable<ScoreInfo> scores; private IEnumerable<ScoreInfo> scores;
public IEnumerable<ScoreInfo> Scores public IEnumerable<ScoreInfo> Scores
@ -60,13 +64,13 @@ namespace osu.Game.Online.Leaderboards
// ensure placeholder is hidden when displaying scores // ensure placeholder is hidden when displaying scores
PlaceholderState = PlaceholderState.Successful; PlaceholderState = PlaceholderState.Successful;
var sf = CreateScoreFlow(); var scoreFlow = CreateScoreFlow();
sf.ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1)); scoreFlow.ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1));
// schedule because we may not be loaded yet (LoadComponentAsync complains). // schedule because we may not be loaded yet (LoadComponentAsync complains).
showScoresDelegate = Schedule(() => LoadComponentAsync(sf, _ => showScoresDelegate = Schedule(() => LoadComponentAsync(scoreFlow, _ =>
{ {
scrollContainer.Add(scrollFlow = sf); scrollContainer.Add(scrollFlow = scoreFlow);
int i = 0; int i = 0;
@ -116,9 +120,7 @@ namespace osu.Game.Online.Leaderboards
{ {
if (value != PlaceholderState.Successful) if (value != PlaceholderState.Successful)
{ {
getScoresRequest?.Cancel(); Reset();
getScoresRequest = null;
Scores = null;
} }
if (value == placeholderState) if (value == placeholderState)
@ -162,12 +164,35 @@ namespace osu.Game.Online.Leaderboards
protected Leaderboard() protected Leaderboard()
{ {
Children = new Drawable[] InternalChildren = new Drawable[]
{ {
scrollContainer = new OsuScrollContainer new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false, RowDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
scrollContainer = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
}
},
new Drawable[]
{
content = new Container
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
},
}
},
}, },
loading = new LoadingAnimation(), loading = new LoadingAnimation(),
placeholderContainer = new Container placeholderContainer = new Container
@ -177,6 +202,13 @@ namespace osu.Game.Online.Leaderboards
}; };
} }
protected virtual void Reset()
{
getScoresRequest?.Cancel();
getScoresRequest = null;
Scores = null;
}
private IAPIProvider api; private IAPIProvider api;
private ScheduledDelegate pendingUpdateScores; private ScheduledDelegate pendingUpdateScores;

View File

@ -20,23 +20,23 @@ using osu.Game.Scoring;
using osu.Game.Users.Drawables; using osu.Game.Users.Drawables;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using Humanizer;
namespace osu.Game.Online.Leaderboards namespace osu.Game.Online.Leaderboards
{ {
public class LeaderboardScore : OsuClickableContainer public class LeaderboardScore : OsuClickableContainer
{ {
public readonly int RankPosition;
public const float HEIGHT = 60; public const float HEIGHT = 60;
private const float corner_radius = 5; private const float corner_radius = 5;
private const float edge_margin = 5; private const float edge_margin = 5;
private const float background_alpha = 0.25f; private const float background_alpha = 0.25f;
private const float rank_width = 30; private const float rank_width = 35;
protected Container RankContainer { get; private set; } protected Container RankContainer { get; private set; }
private readonly ScoreInfo score; private readonly ScoreInfo score;
private readonly int rank;
private Box background; private Box background;
private Container content; private Container content;
@ -52,7 +52,7 @@ namespace osu.Game.Online.Leaderboards
public LeaderboardScore(ScoreInfo score, int rank) public LeaderboardScore(ScoreInfo score, int rank)
{ {
this.score = score; this.score = score;
RankPosition = rank; this.rank = rank;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = HEIGHT; Height = HEIGHT;
@ -79,8 +79,8 @@ namespace osu.Game.Online.Leaderboards
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 22, italics: true), Font = OsuFont.GetFont(size: 20, italics: true),
Text = RankPosition.ToString(), Text = rank.ToMetric(decimals: rank < 100000 ? 1 : 0),
}, },
}, },
}, },

View File

@ -14,6 +14,8 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Users; using osu.Game.Users;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using System.Net;
using osuTK;
namespace osu.Game.Overlays.Changelog namespace osu.Game.Overlays.Changelog
{ {
@ -66,22 +68,34 @@ namespace osu.Game.Overlays.Changelog
foreach (APIChangelogEntry entry in categoryEntries) foreach (APIChangelogEntry entry in categoryEntries)
{ {
LinkFlowContainer title = new LinkFlowContainer
{
Direction = FillDirection.Full,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Vertical = 5 },
};
var entryColour = entry.Major ? colours.YellowLight : Color4.White; var entryColour = entry.Major ? colours.YellowLight : Color4.White;
title.AddIcon(FontAwesome.Solid.Check, t => LinkFlowContainer title;
Container titleContainer = new Container
{ {
t.Font = fontSmall; AutoSizeAxes = Axes.Y,
t.Colour = entryColour; RelativeSizeAxes = Axes.X,
t.Padding = new MarginPadding { Left = -17, Right = 5 }; Margin = new MarginPadding { Vertical = 5 },
}); Children = new Drawable[]
{
new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreRight,
Size = new Vector2(fontSmall.Size),
Icon = entry.Type == ChangelogEntryType.Fix ? FontAwesome.Solid.Check : FontAwesome.Solid.Plus,
Colour = entryColour,
Margin = new MarginPadding { Right = 5 },
},
title = new LinkFlowContainer
{
Direction = FillDirection.Full,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
}
};
title.AddText(entry.Title, t => title.AddText(entry.Title, t =>
{ {
@ -138,7 +152,7 @@ namespace osu.Game.Overlays.Changelog
t.Colour = entryColour; t.Colour = entryColour;
}); });
ChangelogEntries.Add(title); ChangelogEntries.Add(titleContainer);
if (!string.IsNullOrEmpty(entry.MessageHtml)) if (!string.IsNullOrEmpty(entry.MessageHtml))
{ {
@ -149,7 +163,7 @@ namespace osu.Game.Overlays.Changelog
}; };
// todo: use markdown parsing once API returns markdown // todo: use markdown parsing once API returns markdown
message.AddText(Regex.Replace(entry.MessageHtml, @"<(.|\n)*?>", string.Empty), t => message.AddText(WebUtility.HtmlDecode(Regex.Replace(entry.MessageHtml, @"<(.|\n)*?>", string.Empty)), t =>
{ {
t.Font = fontSmall; t.Font = fontSmall;
t.Colour = new Color4(235, 184, 254, 255); t.Colour = new Color4(235, 184, 254, 255);

View File

@ -13,7 +13,8 @@ namespace osu.Game.Overlays
public class DialogOverlay : OsuFocusedOverlayContainer public class DialogOverlay : OsuFocusedOverlayContainer
{ {
private readonly Container dialogContainer; private readonly Container dialogContainer;
private PopupDialog currentDialog;
public PopupDialog CurrentDialog { get; private set; }
public DialogOverlay() public DialogOverlay()
{ {
@ -31,15 +32,15 @@ namespace osu.Game.Overlays
public void Push(PopupDialog dialog) public void Push(PopupDialog dialog)
{ {
if (dialog == currentDialog) return; if (dialog == CurrentDialog) return;
currentDialog?.Hide(); CurrentDialog?.Hide();
currentDialog = dialog; CurrentDialog = dialog;
dialogContainer.Add(currentDialog); dialogContainer.Add(CurrentDialog);
currentDialog.Show(); CurrentDialog.Show();
currentDialog.State.ValueChanged += state => onDialogOnStateChanged(dialog, state.NewValue); CurrentDialog.State.ValueChanged += state => onDialogOnStateChanged(dialog, state.NewValue);
Show(); Show();
} }
@ -52,8 +53,11 @@ namespace osu.Game.Overlays
//handle the dialog being dismissed. //handle the dialog being dismissed.
dialog.Delay(PopupDialog.EXIT_DURATION).Expire(); dialog.Delay(PopupDialog.EXIT_DURATION).Expire();
if (dialog == currentDialog) if (dialog == CurrentDialog)
{
Hide(); Hide();
CurrentDialog = null;
}
} }
protected override void PopIn() protected override void PopIn()
@ -66,9 +70,9 @@ namespace osu.Game.Overlays
{ {
base.PopOut(); base.PopOut();
if (currentDialog?.State.Value == Visibility.Visible) if (CurrentDialog?.State.Value == Visibility.Visible)
{ {
currentDialog.Hide(); CurrentDialog.Hide();
return; return;
} }
@ -80,7 +84,7 @@ namespace osu.Game.Overlays
switch (action) switch (action)
{ {
case GlobalAction.Select: case GlobalAction.Select:
currentDialog?.Buttons.OfType<PopupDialogOkButton>().FirstOrDefault()?.Click(); CurrentDialog?.Buttons.OfType<PopupDialogOkButton>().FirstOrDefault()?.Click();
return true; return true;
} }

View File

@ -51,7 +51,7 @@ namespace osu.Game.Overlays
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
audio.Tracks.RemoveAdjustment(AdjustableProperty.Volume, audioVolume); audio?.Tracks.RemoveAdjustment(AdjustableProperty.Volume, audioVolume);
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }
} }

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
@ -18,7 +19,6 @@ namespace osu.Game.Overlays.Music
public class PlaylistList : CompositeDrawable public class PlaylistList : CompositeDrawable
{ {
public Action<BeatmapSetInfo> Selected; public Action<BeatmapSetInfo> Selected;
public Action<BeatmapSetInfo, int> OrderChanged;
private readonly ItemsScrollContainer items; private readonly ItemsScrollContainer items;
@ -28,7 +28,6 @@ namespace osu.Game.Overlays.Music
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Selected = set => Selected?.Invoke(set), Selected = set => Selected?.Invoke(set),
OrderChanged = (s, i) => OrderChanged?.Invoke(s, i)
}; };
} }
@ -45,13 +44,17 @@ namespace osu.Game.Overlays.Music
private class ItemsScrollContainer : OsuScrollContainer private class ItemsScrollContainer : OsuScrollContainer
{ {
public Action<BeatmapSetInfo> Selected; public Action<BeatmapSetInfo> Selected;
public Action<BeatmapSetInfo, int> OrderChanged;
private readonly SearchContainer search; private readonly SearchContainer search;
private readonly FillFlowContainer<PlaylistItem> items; private readonly FillFlowContainer<PlaylistItem> items;
private readonly IBindable<WorkingBeatmap> beatmapBacking = new Bindable<WorkingBeatmap>(); private readonly IBindable<WorkingBeatmap> beatmapBacking = new Bindable<WorkingBeatmap>();
private IBindableList<BeatmapSetInfo> beatmaps;
[Resolved]
private MusicController musicController { get; set; }
public ItemsScrollContainer() public ItemsScrollContainer()
{ {
Children = new Drawable[] Children = new Drawable[]
@ -73,27 +76,35 @@ namespace osu.Game.Overlays.Music
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(BeatmapManager beatmaps, IBindable<WorkingBeatmap> beatmap) private void load(IBindable<WorkingBeatmap> beatmap)
{ {
beatmaps.GetAllUsableBeatmapSets().ForEach(addBeatmapSet); beatmaps = musicController.BeatmapSets.GetBoundCopy();
beatmaps.ItemAdded += addBeatmapSet; beatmaps.ItemsAdded += i => i.ForEach(addBeatmapSet);
beatmaps.ItemRemoved += removeBeatmapSet; beatmaps.ItemsRemoved += i => i.ForEach(removeBeatmapSet);
beatmaps.ForEach(addBeatmapSet);
beatmapBacking.BindTo(beatmap); beatmapBacking.BindTo(beatmap);
beatmapBacking.ValueChanged += _ => updateSelectedSet(); beatmapBacking.ValueChanged += _ => updateSelectedSet();
} }
private void addBeatmapSet(BeatmapSetInfo obj) => Schedule(() => private void addBeatmapSet(BeatmapSetInfo obj)
{ {
items.Insert(items.Count - 1, new PlaylistItem(obj) { OnSelect = set => Selected?.Invoke(set) }); if (obj == draggedItem?.BeatmapSetInfo) return;
});
private void removeBeatmapSet(BeatmapSetInfo obj) => Schedule(() => Schedule(() => items.Insert(items.Count - 1, new PlaylistItem(obj) { OnSelect = set => Selected?.Invoke(set) }));
}
private void removeBeatmapSet(BeatmapSetInfo obj)
{ {
var itemToRemove = items.FirstOrDefault(i => i.BeatmapSetInfo.ID == obj.ID); if (obj == draggedItem?.BeatmapSetInfo) return;
if (itemToRemove != null)
items.Remove(itemToRemove); Schedule(() =>
}); {
var itemToRemove = items.FirstOrDefault(i => i.BeatmapSetInfo.ID == obj.ID);
if (itemToRemove != null)
items.Remove(itemToRemove);
});
}
private void updateSelectedSet() private void updateSelectedSet()
{ {
@ -112,6 +123,8 @@ namespace osu.Game.Overlays.Music
private Vector2 nativeDragPosition; private Vector2 nativeDragPosition;
private PlaylistItem draggedItem; private PlaylistItem draggedItem;
private int? dragDestination;
protected override bool OnDragStart(DragStartEvent e) protected override bool OnDragStart(DragStartEvent e)
{ {
nativeDragPosition = e.ScreenSpaceMousePosition; nativeDragPosition = e.ScreenSpaceMousePosition;
@ -131,10 +144,17 @@ namespace osu.Game.Overlays.Music
protected override bool OnDragEnd(DragEndEvent e) protected override bool OnDragEnd(DragEndEvent e)
{ {
nativeDragPosition = e.ScreenSpaceMousePosition; nativeDragPosition = e.ScreenSpaceMousePosition;
var handled = draggedItem != null || base.OnDragEnd(e);
draggedItem = null;
return handled; if (draggedItem == null)
return base.OnDragEnd(e);
if (dragDestination != null)
musicController.ChangeBeatmapSetPosition(draggedItem.BeatmapSetInfo, dragDestination.Value);
draggedItem = null;
dragDestination = null;
return true;
} }
protected override void Update() protected override void Update()
@ -210,7 +230,7 @@ namespace osu.Game.Overlays.Music
} }
items.SetLayoutPosition(draggedItem, dstIndex); items.SetLayoutPosition(draggedItem, dstIndex);
OrderChanged?.Invoke(draggedItem.BeatmapSetInfo, dstIndex); dragDestination = dstIndex;
} }
private class ItemSearchContainer : FillFlowContainer<PlaylistItem>, IHasFilterableChildren private class ItemSearchContainer : FillFlowContainer<PlaylistItem>, IHasFilterableChildren

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -22,12 +21,6 @@ namespace osu.Game.Overlays.Music
private const float transition_duration = 600; private const float transition_duration = 600;
private const float playlist_height = 510; private const float playlist_height = 510;
/// <summary>
/// Invoked when the order of an item in the list has changed.
/// The second parameter indicates the new index of the item.
/// </summary>
public Action<BeatmapSetInfo, int> OrderChanged;
private readonly Bindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>(); private readonly Bindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
private BeatmapManager beatmaps; private BeatmapManager beatmaps;
@ -65,7 +58,6 @@ namespace osu.Game.Overlays.Music
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 95, Bottom = 10, Right = 10 }, Padding = new MarginPadding { Top = 95, Bottom = 10, Right = 10 },
Selected = itemSelected, Selected = itemSelected,
OrderChanged = (s, i) => OrderChanged?.Invoke(s, i)
}, },
filter = new FilterControl filter = new FilterControl
{ {

View File

@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.MathUtils;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
@ -24,7 +25,9 @@ namespace osu.Game.Overlays
[Resolved] [Resolved]
private BeatmapManager beatmaps { get; set; } private BeatmapManager beatmaps { get; set; }
private List<BeatmapSetInfo> beatmapSets; public IBindableList<BeatmapSetInfo> BeatmapSets => beatmapSets;
private readonly BindableList<BeatmapSetInfo> beatmapSets = new BindableList<BeatmapSetInfo>();
public bool IsUserPaused { get; private set; } public bool IsUserPaused { get; private set; }
@ -46,7 +49,7 @@ namespace osu.Game.Overlays
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
beatmapSets = beatmaps.GetAllUsableBeatmapSets(); beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()));
beatmaps.ItemAdded += handleBeatmapAdded; beatmaps.ItemAdded += handleBeatmapAdded;
beatmaps.ItemRemoved += handleBeatmapRemoved; beatmaps.ItemRemoved += handleBeatmapRemoved;
} }
@ -140,7 +143,7 @@ namespace osu.Game.Overlays
{ {
queuedDirection = TrackChangeDirection.Prev; queuedDirection = TrackChangeDirection.Prev;
var playable = beatmapSets.TakeWhile(i => i.ID != current.BeatmapSetInfo.ID).LastOrDefault() ?? beatmapSets.LastOrDefault(); var playable = BeatmapSets.TakeWhile(i => i.ID != current.BeatmapSetInfo.ID).LastOrDefault() ?? BeatmapSets.LastOrDefault();
if (playable != null) if (playable != null)
{ {
@ -165,7 +168,7 @@ namespace osu.Game.Overlays
if (!instant) if (!instant)
queuedDirection = TrackChangeDirection.Next; queuedDirection = TrackChangeDirection.Next;
var playable = beatmapSets.SkipWhile(i => i.ID != current.BeatmapSetInfo.ID).Skip(1).FirstOrDefault() ?? beatmapSets.FirstOrDefault(); var playable = BeatmapSets.SkipWhile(i => i.ID != current.BeatmapSetInfo.ID).Skip(1).FirstOrDefault() ?? BeatmapSets.FirstOrDefault();
if (playable != null) if (playable != null)
{ {
@ -200,8 +203,8 @@ namespace osu.Game.Overlays
else else
{ {
//figure out the best direction based on order in playlist. //figure out the best direction based on order in playlist.
var last = beatmapSets.TakeWhile(b => b.ID != current.BeatmapSetInfo?.ID).Count(); var last = BeatmapSets.TakeWhile(b => b.ID != current.BeatmapSetInfo?.ID).Count();
var next = beatmap.NewValue == null ? -1 : beatmapSets.TakeWhile(b => b.ID != beatmap.NewValue.BeatmapSetInfo?.ID).Count(); var next = beatmap.NewValue == null ? -1 : BeatmapSets.TakeWhile(b => b.ID != beatmap.NewValue.BeatmapSetInfo?.ID).Count();
direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next; direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next;
} }

View File

@ -81,7 +81,6 @@ namespace osu.Game.Overlays
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Y = player_height + 10, Y = player_height + 10,
OrderChanged = musicController.ChangeBeatmapSetPosition
}, },
playerContainer = new Container playerContainer = new Container
{ {

View File

@ -28,8 +28,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Rotate cursor when dragging", LabelText = "Hit Lighting",
Bindable = config.GetBindable<bool>(OsuSetting.CursorRotation) Bindable = config.GetBindable<bool>(OsuSetting.HitLighting)
}, },
new SettingsEnumDropdown<ScreenshotFormat> new SettingsEnumDropdown<ScreenshotFormat>
{ {

View File

@ -1,26 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Game.Configuration;
namespace osu.Game.Overlays.Settings.Sections.Graphics
{
public class MainMenuSettings : SettingsSubsection
{
protected override string Header => "User Interface";
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
Children = new[]
{
new SettingsCheckbox
{
LabelText = "Parallax",
Bindable = config.GetBindable<bool>(OsuSetting.MenuParallax)
},
};
}
}
}

View File

@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings.Sections.Graphics
{
public class UserInterfaceSettings : SettingsSubsection
{
protected override string Header => "User Interface";
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
Children = new Drawable[]
{
new SettingsCheckbox
{
LabelText = "Rotate cursor when dragging",
Bindable = config.GetBindable<bool>(OsuSetting.CursorRotation)
},
new SettingsCheckbox
{
LabelText = "Parallax",
Bindable = config.GetBindable<bool>(OsuSetting.MenuParallax)
},
new SettingsSlider<int, TimeSlider>
{
LabelText = "Hold-to-confirm activation time",
Bindable = config.GetBindable<int>(OsuSetting.UIHoldActivationDelay),
KeyboardStep = 50
},
};
}
private class TimeSlider : OsuSliderBar<int>
{
public override string TooltipText => Current.Value.ToString("N0") + "ms";
}
}
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Settings.Sections
new RendererSettings(), new RendererSettings(),
new LayoutSettings(), new LayoutSettings(),
new DetailSettings(), new DetailSettings(),
new MainMenuSettings(), new UserInterfaceSettings(),
}; };
} }
} }

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Judgements
/// </summary> /// </summary>
public class DrawableJudgement : CompositeDrawable public class DrawableJudgement : CompositeDrawable
{ {
private const float judgement_size = 80; private const float judgement_size = 128;
private OsuColour colours; private OsuColour colours;
@ -34,10 +34,14 @@ namespace osu.Game.Rulesets.Judgements
/// <summary> /// <summary>
/// Duration of initial fade in. /// Duration of initial fade in.
/// Default fade out will start immediately after this duration.
/// </summary> /// </summary>
protected virtual double FadeInDuration => 100; protected virtual double FadeInDuration => 100;
/// <summary>
/// Duration to wait until fade out begins. Defaults to <see cref="FadeInDuration"/>.
/// </summary>
protected virtual double FadeOutDelay => FadeInDuration;
/// <summary> /// <summary>
/// Creates a drawable which visualises a <see cref="Judgements.Judgement"/>. /// Creates a drawable which visualises a <see cref="Judgements.Judgement"/>.
/// </summary> /// </summary>
@ -64,10 +68,10 @@ namespace osu.Game.Rulesets.Judgements
Child = new SkinnableDrawable(new GameplaySkinComponent<HitResult>(Result.Type), _ => JudgementText = new OsuSpriteText Child = new SkinnableDrawable(new GameplaySkinComponent<HitResult>(Result.Type), _ => JudgementText = new OsuSpriteText
{ {
Text = Result.Type.GetDescription().ToUpperInvariant(), Text = Result.Type.GetDescription().ToUpperInvariant(),
Font = OsuFont.Numeric.With(size: 12), Font = OsuFont.Numeric.With(size: 20),
Colour = judgementColour(Result.Type), Colour = judgementColour(Result.Type),
Scale = new Vector2(0.85f, 1), Scale = new Vector2(0.85f, 1),
}) }, confineMode: ConfineMode.NoScaling)
}; };
} }
@ -76,7 +80,7 @@ namespace osu.Game.Rulesets.Judgements
JudgementBody.ScaleTo(0.9f); JudgementBody.ScaleTo(0.9f);
JudgementBody.ScaleTo(1, 500, Easing.OutElastic); JudgementBody.ScaleTo(1, 500, Easing.OutElastic);
this.Delay(FadeInDuration).FadeOut(400); this.Delay(FadeOutDelay).FadeOut(400);
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -12,5 +12,10 @@ namespace osu.Game.Rulesets.Mods
/// Whether we should allow failing at the current point in time. /// Whether we should allow failing at the current point in time.
/// </summary> /// </summary>
bool AllowFail { get; } bool AllowFail { get; }
/// <summary>
/// Whether we want to restart on fail. Only used if <see cref="AllowFail"/> is true.
/// </summary>
bool RestartOnFail { get; }
} }
} }

View File

@ -26,7 +26,10 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.Automation; public override ModType Type => ModType.Automation;
public override string Description => "Watch a perfect automated play through the song."; public override string Description => "Watch a perfect automated play through the song.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public bool AllowFail => false; public bool AllowFail => false;
public bool RestartOnFail => false;
public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) }; public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) };
public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;

View File

@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Mods
/// </summary> /// </summary>
public bool AllowFail => false; public bool AllowFail => false;
public bool RestartOnFail => false;
public void ReadFromConfig(OsuConfigManager config) public void ReadFromConfig(OsuConfigManager config)
{ {
showHealthBar = config.GetBindable<bool>(OsuSetting.ShowHealthDisplayWhenCantFail); showHealthBar = config.GetBindable<bool>(OsuSetting.ShowHealthDisplayWhenCantFail);

View File

@ -2,13 +2,16 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModEasy : Mod, IApplicableToDifficulty public abstract class ModEasy : Mod, IApplicableToDifficulty, IApplicableFailOverride, IApplicableToScoreProcessor
{ {
public override string Name => "Easy"; public override string Name => "Easy";
public override string Acronym => "EZ"; public override string Acronym => "EZ";
@ -18,6 +21,10 @@ namespace osu.Game.Rulesets.Mods
public override bool Ranked => true; public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) }; public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) };
private int retries = 2;
private BindableNumber<double> health;
public void ApplyToDifficulty(BeatmapDifficulty difficulty) public void ApplyToDifficulty(BeatmapDifficulty difficulty)
{ {
const float ratio = 0.5f; const float ratio = 0.5f;
@ -26,5 +33,27 @@ namespace osu.Game.Rulesets.Mods
difficulty.DrainRate *= ratio; difficulty.DrainRate *= ratio;
difficulty.OverallDifficulty *= ratio; difficulty.OverallDifficulty *= ratio;
} }
public bool AllowFail
{
get
{
if (retries == 0) return true;
health.Value = health.MaxValue;
retries--;
return false;
}
}
public bool RestartOnFail => false;
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
{
health = scoreProcessor.Health.GetBoundCopy();
}
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
} }
} }

View File

@ -10,7 +10,7 @@ using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModSuddenDeath : Mod, IApplicableToScoreProcessor public abstract class ModSuddenDeath : Mod, IApplicableToScoreProcessor, IApplicableFailOverride
{ {
public override string Name => "Sudden Death"; public override string Name => "Sudden Death";
public override string Acronym => "SD"; public override string Acronym => "SD";
@ -21,6 +21,9 @@ namespace osu.Game.Rulesets.Mods
public override bool Ranked => true; public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) }; public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) };
public bool AllowFail => true;
public bool RestartOnFail => true;
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
{ {
scoreProcessor.FailConditions += FailCondition; scoreProcessor.FailConditions += FailCondition;

View File

@ -240,7 +240,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
#endregion #endregion
protected override void SkinChanged(ISkinSource skin, bool allowFallback) protected sealed override void SkinChanged(ISkinSource skin, bool allowFallback)
{ {
base.SkinChanged(skin, allowFallback); base.SkinChanged(skin, allowFallback);
@ -250,6 +250,20 @@ namespace osu.Game.Rulesets.Objects.Drawables
AccentColour.Value = comboColours?.Count > 0 ? comboColours[combo.ComboIndex % comboColours.Count] : Color4.White; AccentColour.Value = comboColours?.Count > 0 ? comboColours[combo.ComboIndex % comboColours.Count] : Color4.White;
} }
ApplySkin(skin, allowFallback);
if (IsLoaded)
updateState(State.Value, true);
}
/// <summary>
/// Called when a change is made to the skin.
/// </summary>
/// <param name="skin">The new skin.</param>
/// <param name="allowFallback">Whether fallback to default skin should be allowed if the custom skin is missing this resource.</param>
protected virtual void ApplySkin(ISkinSource skin, bool allowFallback)
{
} }
/// <summary> /// <summary>

View File

@ -84,7 +84,7 @@ namespace osu.Game.Rulesets
public virtual Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.Solid.QuestionCircle }; public virtual Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.Solid.QuestionCircle };
public virtual IResourceStore<byte[]> CreateReourceStore() => new NamespacedResourceStore<byte[]>(new DllResourceStore(GetType().Assembly.Location), @"Resources"); public virtual IResourceStore<byte[]> CreateResourceStore() => new NamespacedResourceStore<byte[]>(new DllResourceStore(GetType().Assembly.Location), @"Resources");
public abstract string Description { get; } public abstract string Description { get; }

View File

@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.UI
{ {
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
var resources = Ruleset.CreateReourceStore(); var resources = Ruleset.CreateResourceStore();
if (resources != null) if (resources != null)
{ {

View File

@ -137,9 +137,9 @@ namespace osu.Game.Rulesets.UI
{ {
} }
public bool OnPressed(T action) => Target.Children.OfType<KeyCounterAction<T>>().Any(c => c.OnPressed(action)); public bool OnPressed(T action) => Target.Children.OfType<KeyCounterAction<T>>().Any(c => c.OnPressed(action, Clock.ElapsedFrameTime > 0));
public bool OnReleased(T action) => Target.Children.OfType<KeyCounterAction<T>>().Any(c => c.OnReleased(action)); public bool OnReleased(T action) => Target.Children.OfType<KeyCounterAction<T>>().Any(c => c.OnReleased(action, Clock.ElapsedFrameTime > 0));
} }
#endregion #endregion

View File

@ -22,6 +22,6 @@ namespace osu.Game.Scoring.Legacy
} }
protected override Ruleset GetRuleset(int rulesetId) => rulesets.GetRuleset(rulesetId).CreateInstance(); protected override Ruleset GetRuleset(int rulesetId) => rulesets.GetRuleset(rulesetId).CreateInstance();
protected override WorkingBeatmap GetBeatmap(string md5Hash) => beatmaps.GetWorkingBeatmap(beatmaps.QueryBeatmap(b => b.MD5Hash == md5Hash)); protected override WorkingBeatmap GetBeatmap(string md5Hash) => beatmaps.GetWorkingBeatmap(beatmaps.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.MD5Hash == md5Hash));
} }
} }

View File

@ -0,0 +1,132 @@
// 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.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osuTK;
namespace osu.Game.Screens.Edit.Setup.Components.LabelledComponents
{
public abstract class LabelledComponent : CompositeDrawable
{
protected const float CONTENT_PADDING_VERTICAL = 10;
protected const float CONTENT_PADDING_HORIZONTAL = 15;
protected const float CORNER_RADIUS = 15;
/// <summary>
/// The component that is being displayed.
/// </summary>
protected readonly Drawable Component;
private readonly OsuTextFlowContainer labelText;
private readonly OsuTextFlowContainer descriptionText;
/// <summary>
/// Creates a new <see cref="LabelledComponent"/>.
/// </summary>
/// <param name="padded">Whether the component should be padded or should be expanded to the bounds of this <see cref="LabelledComponent"/>.</param>
protected LabelledComponent(bool padded)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
CornerRadius = CORNER_RADIUS;
Masking = true;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.FromHex("1c2125"),
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Padding = padded
? new MarginPadding { Horizontal = CONTENT_PADDING_HORIZONTAL, Vertical = CONTENT_PADDING_VERTICAL }
: new MarginPadding { Left = CONTENT_PADDING_HORIZONTAL },
Spacing = new Vector2(0, 12),
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[]
{
new Drawable[]
{
labelText = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Bold))
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = 20 }
},
new Container
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = Component = CreateComponent().With(d =>
{
d.Anchor = Anchor.CentreRight;
d.Origin = Anchor.CentreRight;
})
}
},
},
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
},
descriptionText = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold, italics: true))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Bottom = padded ? 0 : CONTENT_PADDING_VERTICAL },
Alpha = 0,
}
}
}
};
}
[BackgroundDependencyLoader]
private void load(OsuColour osuColour)
{
descriptionText.Colour = osuColour.Yellow;
}
public string Label
{
set => labelText.Text = value;
}
public string Description
{
set
{
descriptionText.Text = value;
if (!string.IsNullOrEmpty(value))
descriptionText.Show();
else
descriptionText.Hide();
}
}
/// <summary>
/// Creates the component that should be displayed.
/// </summary>
/// <returns>The component.</returns>
protected abstract Drawable CreateComponent();
}
}

View File

@ -31,6 +31,8 @@ namespace osu.Game.Screens.Menu
{ {
public event Action<ButtonState> StateChanged; public event Action<ButtonState> StateChanged;
public readonly Key TriggerKey;
private readonly Container iconText; private readonly Container iconText;
private readonly Container box; private readonly Container box;
private readonly Box boxHoverLayer; private readonly Box boxHoverLayer;
@ -43,7 +45,6 @@ namespace osu.Game.Screens.Menu
public ButtonSystemState VisibleState = ButtonSystemState.TopLevel; public ButtonSystemState VisibleState = ButtonSystemState.TopLevel;
private readonly Action clickAction; private readonly Action clickAction;
private readonly Key triggerKey;
private SampleChannel sampleClick; private SampleChannel sampleClick;
private SampleChannel sampleHover; private SampleChannel sampleHover;
@ -53,7 +54,7 @@ namespace osu.Game.Screens.Menu
{ {
this.sampleName = sampleName; this.sampleName = sampleName;
this.clickAction = clickAction; this.clickAction = clickAction;
this.triggerKey = triggerKey; TriggerKey = triggerKey;
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
Alpha = 0; Alpha = 0;
@ -210,7 +211,7 @@ namespace osu.Game.Screens.Menu
if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed) if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed)
return false; return false;
if (triggerKey == e.Key && triggerKey != Key.Unknown) if (TriggerKey == e.Key && TriggerKey != Key.Unknown)
{ {
trigger(); trigger();
return true; return true;

View File

@ -14,6 +14,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Threading; using osu.Framework.Threading;
@ -180,6 +181,20 @@ namespace osu.Game.Screens.Menu
State = ButtonSystemState.Initial; State = ButtonSystemState.Initial;
} }
protected override bool OnKeyDown(KeyDownEvent e)
{
if (State == ButtonSystemState.Initial)
{
if (buttonsTopLevel.Any(b => e.Key == b.TriggerKey))
{
logo?.Click();
return true;
}
}
return base.OnKeyDown(e);
}
public bool OnPressed(GlobalAction action) public bool OnPressed(GlobalAction action)
{ {
switch (action) switch (action)

View File

@ -173,7 +173,11 @@ namespace osu.Game.Screens.Menu
.Then(5500) .Then(5500)
.FadeOut(250) .FadeOut(250)
.ScaleTo(0.9f, 250, Easing.InQuint) .ScaleTo(0.9f, 250, Easing.InQuint)
.Finally(d => this.Push(nextScreen)); .Finally(d =>
{
if (nextScreen != null)
this.Push(nextScreen);
});
} }
} }
} }

View File

@ -9,6 +9,10 @@ namespace osu.Game.Screens.Menu
{ {
public class ExitConfirmOverlay : HoldToConfirmOverlay, IKeyBindingHandler<GlobalAction> public class ExitConfirmOverlay : HoldToConfirmOverlay, IKeyBindingHandler<GlobalAction>
{ {
protected override bool AllowMultipleFires => true;
public void Abort() => AbortConfirm();
public bool OnPressed(GlobalAction action) public bool OnPressed(GlobalAction action)
{ {
if (action == GlobalAction.Back) if (action == GlobalAction.Back)
@ -24,7 +28,8 @@ namespace osu.Game.Screens.Menu
{ {
if (action == GlobalAction.Back) if (action == GlobalAction.Back)
{ {
AbortConfirm(); if (!Fired)
AbortConfirm();
return true; return true;
} }

View File

@ -1,18 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Charts; using osu.Game.Screens.Charts;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
@ -51,15 +55,35 @@ namespace osu.Game.Screens.Menu
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
[Resolved(canBeNull: true)]
private DialogOverlay dialogOverlay { get; set; }
private BackgroundScreenDefault background; private BackgroundScreenDefault background;
protected override BackgroundScreen CreateBackground() => background; protected override BackgroundScreen CreateBackground() => background;
private Bindable<int> holdDelay;
private ExitConfirmOverlay exitConfirmOverlay;
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(DirectOverlay direct, SettingsOverlay settings) private void load(DirectOverlay direct, SettingsOverlay settings, OsuConfigManager config)
{ {
holdDelay = config.GetBindable<int>(OsuSetting.UIHoldActivationDelay);
if (host.CanExit) if (host.CanExit)
AddInternal(new ExitConfirmOverlay { Action = this.Exit }); {
AddInternal(exitConfirmOverlay = new ExitConfirmOverlay
{
Action = () =>
{
if (holdDelay.Value > 0)
confirmAndExit();
else
this.Exit();
}
});
}
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {
@ -74,7 +98,7 @@ namespace osu.Game.Screens.Menu
OnEdit = delegate { this.Push(new Editor()); }, OnEdit = delegate { this.Push(new Editor()); },
OnSolo = onSolo, OnSolo = onSolo,
OnMulti = delegate { this.Push(new Multiplayer()); }, OnMulti = delegate { this.Push(new Multiplayer()); },
OnExit = this.Exit, OnExit = confirmAndExit,
} }
} }
}, },
@ -103,6 +127,12 @@ namespace osu.Game.Screens.Menu
preloadSongSelect(); preloadSongSelect();
} }
private void confirmAndExit()
{
exitConfirmed = true;
this.Exit();
}
private void preloadSongSelect() private void preloadSongSelect()
{ {
if (songSelect == null) if (songSelect == null)
@ -141,6 +171,7 @@ namespace osu.Game.Screens.Menu
} }
private bool loginDisplayed; private bool loginDisplayed;
private bool exitConfirmed;
protected override void LogoArriving(OsuLogo logo, bool resuming) protected override void LogoArriving(OsuLogo logo, bool resuming)
{ {
@ -221,9 +252,40 @@ namespace osu.Game.Screens.Menu
public override bool OnExiting(IScreen next) public override bool OnExiting(IScreen next)
{ {
if (!exitConfirmed && dialogOverlay != null && !(dialogOverlay.CurrentDialog is ConfirmExitDialog))
{
dialogOverlay.Push(new ConfirmExitDialog(confirmAndExit, () => exitConfirmOverlay.Abort()));
return true;
}
buttons.State = ButtonSystemState.Exit; buttons.State = ButtonSystemState.Exit;
this.FadeOut(3000); this.FadeOut(3000);
return base.OnExiting(next); return base.OnExiting(next);
} }
private class ConfirmExitDialog : PopupDialog
{
public ConfirmExitDialog(Action confirm, Action cancel)
{
HeaderText = "Are you sure you want to exit?";
BodyText = "Last chance to back out.";
Icon = FontAwesome.Solid.ExclamationTriangle;
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton
{
Text = @"Good bye",
Action = confirm
},
new PopupDialogCancelButton
{
Text = @"Just a little more",
Action = cancel
},
};
}
}
} }
} }

View File

@ -42,5 +42,7 @@ namespace osu.Game.Screens.Play
public double FramesPerSecond => underlyingClock.FramesPerSecond; public double FramesPerSecond => underlyingClock.FramesPerSecond;
public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo; public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo;
public IClock Source => underlyingClock;
} }
} }

View File

@ -231,7 +231,6 @@ namespace osu.Game.Screens.Play
protected virtual KeyCounterDisplay CreateKeyCounter() => new KeyCounterDisplay protected virtual KeyCounterDisplay CreateKeyCounter() => new KeyCounterDisplay
{ {
FadeTime = 50,
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Margin = new MarginPadding(10), Margin = new MarginPadding(10),

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -22,9 +20,6 @@ namespace osu.Game.Screens.Play
private Container textLayer; private Container textLayer;
private SpriteText countSpriteText; private SpriteText countSpriteText;
private readonly List<KeyCounterState> states = new List<KeyCounterState>();
private KeyCounterState currentState;
public bool IsCounting { get; set; } = true; public bool IsCounting { get; set; } = true;
private int countPresses; private int countPresses;
@ -52,20 +47,30 @@ namespace osu.Game.Screens.Play
{ {
isLit = value; isLit = value;
updateGlowSprite(value); updateGlowSprite(value);
if (value && IsCounting)
{
CountPresses++;
saveState();
}
} }
} }
} }
public void Increment()
{
if (!IsCounting)
return;
CountPresses++;
}
public void Decrement()
{
if (!IsCounting)
return;
CountPresses--;
}
//further: change default values here and in KeyCounterCollection if needed, instead of passing them in every constructor //further: change default values here and in KeyCounterCollection if needed, instead of passing them in every constructor
public Color4 KeyDownTextColor { get; set; } = Color4.DarkGray; public Color4 KeyDownTextColor { get; set; } = Color4.DarkGray;
public Color4 KeyUpTextColor { get; set; } = Color4.White; public Color4 KeyUpTextColor { get; set; } = Color4.White;
public int FadeTime { get; set; } public double FadeTime { get; set; }
protected KeyCounter(string name) protected KeyCounter(string name)
{ {
@ -73,11 +78,8 @@ namespace osu.Game.Screens.Play
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(TextureStore textures, GameplayClock clock) private void load(TextureStore textures)
{ {
if (clock != null)
Clock = clock;
Children = new Drawable[] Children = new Drawable[]
{ {
buttonSprite = new Sprite buttonSprite = new Sprite
@ -132,42 +134,16 @@ namespace osu.Game.Screens.Play
{ {
if (show) if (show)
{ {
glowSprite.FadeIn(FadeTime); double remainingFadeTime = FadeTime * (1 - glowSprite.Alpha);
textLayer.FadeColour(KeyDownTextColor, FadeTime); glowSprite.FadeIn(remainingFadeTime, Easing.OutQuint);
textLayer.FadeColour(KeyDownTextColor, remainingFadeTime, Easing.OutQuint);
} }
else else
{ {
glowSprite.FadeOut(FadeTime); double remainingFadeTime = 8 * FadeTime * glowSprite.Alpha;
textLayer.FadeColour(KeyUpTextColor, FadeTime); glowSprite.FadeOut(remainingFadeTime, Easing.OutQuint);
textLayer.FadeColour(KeyUpTextColor, remainingFadeTime, Easing.OutQuint);
} }
} }
public void ResetCount()
{
CountPresses = 0;
states.Clear();
}
protected override void Update()
{
base.Update();
if (currentState?.Time > Clock.CurrentTime)
restoreStateTo(Clock.CurrentTime);
}
private void saveState()
{
if (currentState == null || currentState.Time < Clock.CurrentTime)
states.Add(currentState = new KeyCounterState(Clock.CurrentTime, CountPresses));
}
private void restoreStateTo(double time)
{
states.RemoveAll(state => state.Time > time);
currentState = states.LastOrDefault();
CountPresses = currentState?.Count ?? 0;
}
} }
} }

View File

@ -1,11 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Input.Bindings;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
public class KeyCounterAction<T> : KeyCounter, IKeyBindingHandler<T> public class KeyCounterAction<T> : KeyCounter
where T : struct where T : struct
{ {
public T Action { get; } public T Action { get; }
@ -16,15 +14,25 @@ namespace osu.Game.Screens.Play
Action = action; Action = action;
} }
public bool OnPressed(T action) public bool OnPressed(T action, bool forwards)
{ {
if (action.Equals(Action)) IsLit = true; if (!action.Equals(Action))
return false;
IsLit = true;
if (forwards)
Increment();
return false; return false;
} }
public bool OnReleased(T action) public bool OnReleased(T action, bool forwards)
{ {
if (action.Equals(Action)) IsLit = false; if (!action.Equals(Action))
return false;
IsLit = false;
if (!forwards)
Decrement();
return false; return false;
} }
} }

View File

@ -17,6 +17,7 @@ namespace osu.Game.Screens.Play
public class KeyCounterDisplay : FillFlowContainer<KeyCounter> public class KeyCounterDisplay : FillFlowContainer<KeyCounter>
{ {
private const int duration = 100; private const int duration = 100;
private const double key_fade_time = 80;
public readonly Bindable<bool> Visible = new Bindable<bool>(true); public readonly Bindable<bool> Visible = new Bindable<bool>(true);
private readonly Bindable<bool> configVisibility = new Bindable<bool>(); private readonly Bindable<bool> configVisibility = new Bindable<bool>();
@ -33,17 +34,11 @@ namespace osu.Game.Screens.Play
base.Add(key); base.Add(key);
key.IsCounting = IsCounting; key.IsCounting = IsCounting;
key.FadeTime = FadeTime; key.FadeTime = key_fade_time;
key.KeyDownTextColor = KeyDownTextColor; key.KeyDownTextColor = KeyDownTextColor;
key.KeyUpTextColor = KeyUpTextColor; key.KeyUpTextColor = KeyUpTextColor;
} }
public void ResetCount()
{
foreach (var counter in Children)
counter.ResetCount();
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
{ {
@ -68,22 +63,6 @@ namespace osu.Game.Screens.Play
} }
} }
private int fadeTime;
public int FadeTime
{
get => fadeTime;
set
{
if (value != fadeTime)
{
fadeTime = value;
foreach (var child in Children)
child.FadeTime = value;
}
}
}
private Color4 keyDownTextColor = Color4.DarkGray; private Color4 keyDownTextColor = Color4.DarkGray;
public Color4 KeyDownTextColor public Color4 KeyDownTextColor
@ -123,11 +102,6 @@ namespace osu.Game.Screens.Play
private Receptor receptor; private Receptor receptor;
public Receptor GetReceptor()
{
return receptor ?? (receptor = new Receptor(this));
}
public void SetReceptor(Receptor receptor) public void SetReceptor(Receptor receptor)
{ {
if (this.receptor != null) if (this.receptor != null)

View File

@ -18,7 +18,12 @@ namespace osu.Game.Screens.Play
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)
{ {
if (e.Key == Key) IsLit = true; if (e.Key == Key)
{
IsLit = true;
Increment();
}
return base.OnKeyDown(e); return base.OnKeyDown(e);
} }

View File

@ -36,7 +36,12 @@ namespace osu.Game.Screens.Play
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
{ {
if (e.Button == Button) IsLit = true; if (e.Button == Button)
{
IsLit = true;
Increment();
}
return base.OnMouseDown(e); return base.OnMouseDown(e);
} }

View File

@ -86,6 +86,12 @@ namespace osu.Game.Screens.Play
[Cached(Type = typeof(IBindable<IReadOnlyList<Mod>>))] [Cached(Type = typeof(IBindable<IReadOnlyList<Mod>>))]
protected new readonly Bindable<IReadOnlyList<Mod>> Mods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>()); protected new readonly Bindable<IReadOnlyList<Mod>> Mods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
/// <summary>
/// Whether failing should be allowed.
/// By default, this checks whether all selected mods allow failing.
/// </summary>
protected virtual bool AllowFail => Mods.Value.OfType<IApplicableFailOverride>().All(m => m.AllowFail);
private readonly bool allowPause; private readonly bool allowPause;
private readonly bool showResults; private readonly bool showResults;
@ -360,7 +366,7 @@ namespace osu.Game.Screens.Play
private bool onFail() private bool onFail()
{ {
if (Mods.Value.OfType<IApplicableFailOverride>().Any(m => !m.AllowFail)) if (!AllowFail)
return false; return false;
HasFailed = true; HasFailed = true;
@ -372,6 +378,10 @@ namespace osu.Game.Screens.Play
PauseOverlay.Hide(); PauseOverlay.Hide();
failAnimation.Start(); failAnimation.Start();
if (Mods.Value.OfType<IApplicableFailOverride>().Any(m => m.RestartOnFail))
Restart();
return true; return true;
} }

View File

@ -9,6 +9,9 @@ namespace osu.Game.Screens.Play
{ {
private readonly Score score; private readonly Score score;
// Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108)
protected override bool AllowFail => false;
public ReplayPlayer(Score score, bool allowPause = true, bool showResults = true) public ReplayPlayer(Score score, bool allowPause = true, bool showResults = true)
: base(allowPause, showResults) : base(allowPause, showResults)
{ {

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.MathUtils; using osu.Framework.MathUtils;
using osu.Framework.Threading;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
@ -121,6 +122,12 @@ namespace osu.Game.Screens.Play
handleBase.X = newX; handleBase.X = newX;
} }
protected override void OnUserChange(double value) => OnSeek?.Invoke(value); private ScheduledDelegate scheduledSeek;
protected override void OnUserChange(double value)
{
scheduledSeek?.Cancel();
scheduledSeek = Schedule(() => OnSeek?.Invoke(value));
}
} }
} }

View File

@ -49,7 +49,7 @@ namespace osu.Game.Screens.Select
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Text = @"Mods", Text = @"Selected Mods",
Alpha = 0, Alpha = 0,
}, },
}; };

View File

@ -24,12 +24,30 @@ namespace osu.Game.Screens.Select.Carousel
{ {
base.Filter(criteria); base.Filter(criteria);
bool match = criteria.Ruleset == null || Beatmap.RulesetID == criteria.Ruleset.ID || (Beatmap.RulesetID == 0 && criteria.Ruleset.ID > 0 && criteria.AllowConvertedBeatmaps); bool match =
criteria.Ruleset == null ||
Beatmap.RulesetID == criteria.Ruleset.ID ||
(Beatmap.RulesetID == 0 && criteria.Ruleset.ID > 0 && criteria.AllowConvertedBeatmaps);
foreach (var criteriaTerm in criteria.SearchTerms) match &= criteria.StarDifficulty.IsInRange(Beatmap.StarDifficulty);
match &= match &= criteria.ApproachRate.IsInRange(Beatmap.BaseDifficulty.ApproachRate);
Beatmap.Metadata.SearchableTerms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0) || match &= criteria.DrainRate.IsInRange(Beatmap.BaseDifficulty.DrainRate);
Beatmap.Version.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0; match &= criteria.CircleSize.IsInRange(Beatmap.BaseDifficulty.CircleSize);
match &= criteria.Length.IsInRange(Beatmap.Length);
match &= criteria.BPM.IsInRange(Beatmap.BPM);
match &= criteria.BeatDivisor.IsInRange(Beatmap.BeatDivisor);
match &= criteria.OnlineStatus.IsInRange(Beatmap.Status);
match &= criteria.Creator.Matches(Beatmap.Metadata.AuthorString);
match &= criteria.Artist.Matches(Beatmap.Metadata.Artist) ||
criteria.Artist.Matches(Beatmap.Metadata.ArtistUnicode);
if (match)
foreach (var criteriaTerm in criteria.SearchTerms)
match &=
Beatmap.Metadata.SearchableTerms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0) ||
Beatmap.Version.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0;
Filtered.Value = !match; Filtered.Value = !match;
} }

View File

@ -33,14 +33,21 @@ namespace osu.Game.Screens.Select
private Bindable<GroupMode> groupMode; private Bindable<GroupMode> groupMode;
public FilterCriteria CreateCriteria() => new FilterCriteria public FilterCriteria CreateCriteria()
{ {
Group = groupMode.Value, var query = searchTextBox.Text;
Sort = sortMode.Value,
SearchText = searchTextBox.Text, var criteria = new FilterCriteria
AllowConvertedBeatmaps = showConverted.Value, {
Ruleset = ruleset.Value Group = groupMode.Value,
}; Sort = sortMode.Value,
AllowConvertedBeatmaps = showConverted.Value,
Ruleset = ruleset.Value
};
FilterQueryParser.ApplyQueries(criteria, query);
return criteria;
}
public Action Exit; public Action Exit;

View File

@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Screens.Select.Filter; using osu.Game.Screens.Select.Filter;
@ -13,6 +15,17 @@ namespace osu.Game.Screens.Select
public GroupMode Group; public GroupMode Group;
public SortMode Sort; public SortMode Sort;
public OptionalRange<double> StarDifficulty;
public OptionalRange<float> ApproachRate;
public OptionalRange<float> DrainRate;
public OptionalRange<float> CircleSize;
public OptionalRange<double> Length;
public OptionalRange<double> BPM;
public OptionalRange<int> BeatDivisor;
public OptionalRange<BeatmapSetOnlineStatus> OnlineStatus;
public OptionalTextFilter Creator;
public OptionalTextFilter Artist;
public string[] SearchTerms = Array.Empty<string>(); public string[] SearchTerms = Array.Empty<string>();
public RulesetInfo Ruleset; public RulesetInfo Ruleset;
@ -26,8 +39,69 @@ namespace osu.Game.Screens.Select
set set
{ {
searchText = value; searchText = value;
SearchTerms = searchText.Split(',', ' ', '!').Where(s => !string.IsNullOrEmpty(s)).ToArray(); SearchTerms = searchText.Split(new[] { ',', ' ', '!' }, StringSplitOptions.RemoveEmptyEntries).ToArray();
} }
} }
public struct OptionalRange<T> : IEquatable<OptionalRange<T>>
where T : struct, IComparable
{
public bool IsInRange(T value)
{
if (Min != null)
{
int comparison = Comparer<T>.Default.Compare(value, Min.Value);
if (comparison < 0)
return false;
if (comparison == 0 && !IsLowerInclusive)
return false;
}
if (Max != null)
{
int comparison = Comparer<T>.Default.Compare(value, Max.Value);
if (comparison > 0)
return false;
if (comparison == 0 && !IsUpperInclusive)
return false;
}
return true;
}
public T? Min;
public T? Max;
public bool IsLowerInclusive;
public bool IsUpperInclusive;
public bool Equals(OptionalRange<T> other)
=> Min.Equals(other.Min)
&& Max.Equals(other.Max)
&& IsLowerInclusive.Equals(other.IsLowerInclusive)
&& IsUpperInclusive.Equals(other.IsUpperInclusive);
}
public struct OptionalTextFilter : IEquatable<OptionalTextFilter>
{
public bool Matches(string value)
{
if (string.IsNullOrEmpty(SearchTerm))
return true;
// search term is guaranteed to be non-empty, so if the string we're comparing is empty, it's not matching
if (string.IsNullOrEmpty(value))
return false;
return value.IndexOf(SearchTerm, StringComparison.InvariantCultureIgnoreCase) >= 0;
}
public string SearchTerm;
public bool Equals(OptionalTextFilter other) => SearchTerm?.Equals(other.SearchTerm) ?? true;
}
} }
} }

View File

@ -0,0 +1,211 @@
// 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.Globalization;
using System.Text.RegularExpressions;
using osu.Game.Beatmaps;
namespace osu.Game.Screens.Select
{
internal static class FilterQueryParser
{
private static readonly Regex query_syntax_regex = new Regex(
@"\b(?<key>stars|ar|dr|cs|divisor|length|objects|bpm|status|creator|artist)(?<op>[=:><]+)(?<value>("".*"")|(\S*))",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
internal static void ApplyQueries(FilterCriteria criteria, string query)
{
foreach (Match match in query_syntax_regex.Matches(query))
{
var key = match.Groups["key"].Value.ToLower();
var op = match.Groups["op"].Value;
var value = match.Groups["value"].Value;
parseKeywordCriteria(criteria, key, value, op);
query = query.Replace(match.ToString(), "");
}
criteria.SearchText = query;
}
private static void parseKeywordCriteria(FilterCriteria criteria, string key, string value, string op)
{
switch (key)
{
case "stars" when parseFloatWithPoint(value, out var stars):
updateCriteriaRange(ref criteria.StarDifficulty, op, stars, 0.01f / 2);
break;
case "ar" when parseFloatWithPoint(value, out var ar):
updateCriteriaRange(ref criteria.ApproachRate, op, ar, 0.1f / 2);
break;
case "dr" when parseFloatWithPoint(value, out var dr):
updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2);
break;
case "cs" when parseFloatWithPoint(value, out var cs):
updateCriteriaRange(ref criteria.CircleSize, op, cs, 0.1f / 2);
break;
case "bpm" when parseDoubleWithPoint(value, out var bpm):
updateCriteriaRange(ref criteria.BPM, op, bpm, 0.01d / 2);
break;
case "length" when parseDoubleWithPoint(value.TrimEnd('m', 's', 'h'), out var length):
var scale = getLengthScale(value);
updateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0);
break;
case "divisor" when parseInt(value, out var divisor):
updateCriteriaRange(ref criteria.BeatDivisor, op, divisor);
break;
case "status" when Enum.TryParse<BeatmapSetOnlineStatus>(value, true, out var statusValue):
updateCriteriaRange(ref criteria.OnlineStatus, op, statusValue);
break;
case "creator":
updateCriteriaText(ref criteria.Creator, op, value);
break;
case "artist":
updateCriteriaText(ref criteria.Artist, op, value);
break;
}
}
private static int getLengthScale(string value) =>
value.EndsWith("ms") ? 1 :
value.EndsWith("s") ? 1000 :
value.EndsWith("m") ? 60000 :
value.EndsWith("h") ? 3600000 : 1000;
private static bool parseFloatWithPoint(string value, out float result) =>
float.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result);
private static bool parseDoubleWithPoint(string value, out double result) =>
double.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result);
private static bool parseInt(string value, out int result) =>
int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result);
private static void updateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, string op, string value)
{
switch (op)
{
case "=":
case ":":
textFilter.SearchTerm = value.Trim('"');
break;
}
}
private static void updateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, string op, float value, float tolerance = 0.05f)
{
switch (op)
{
default:
return;
case "=":
case ":":
range.Min = value - tolerance;
range.Max = value + tolerance;
break;
case ">":
range.Min = value + tolerance;
break;
case ">=":
case ">:":
range.Min = value - tolerance;
break;
case "<":
range.Max = value - tolerance;
break;
case "<=":
case "<:":
range.Max = value + tolerance;
break;
}
}
private static void updateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, string op, double value, double tolerance = 0.05)
{
switch (op)
{
default:
return;
case "=":
case ":":
range.Min = value - tolerance;
range.Max = value + tolerance;
break;
case ">":
range.Min = value + tolerance;
break;
case ">=":
case ">:":
range.Min = value - tolerance;
break;
case "<":
range.Max = value - tolerance;
break;
case "<=":
case "<:":
range.Max = value + tolerance;
break;
}
}
private static void updateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, string op, T value)
where T : struct, IComparable
{
switch (op)
{
default:
return;
case "=":
case ":":
range.IsLowerInclusive = range.IsUpperInclusive = true;
range.Min = value;
range.Max = value;
break;
case ">":
range.IsLowerInclusive = false;
range.Min = value;
break;
case ">=":
case ">:":
range.IsLowerInclusive = true;
range.Min = value;
break;
case "<":
range.IsUpperInclusive = false;
range.Max = value;
break;
case "<=":
case "<:":
range.IsUpperInclusive = true;
range.Max = value;
break;
}
}
}
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards; using osu.Game.Online.Leaderboards;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -37,8 +38,25 @@ namespace osu.Game.Screens.Select.Leaderboards
} }
} }
public APILegacyUserTopScoreInfo TopScore
{
get => topScoreContainer.Score.Value;
set
{
if (value == null)
topScoreContainer.Hide();
else
{
topScoreContainer.Show();
topScoreContainer.Score.Value = value;
}
}
}
private bool filterMods; private bool filterMods;
private UserTopScoreContainer topScoreContainer;
/// <summary> /// <summary>
/// Whether to apply the game's currently selected mods as a filter when retrieving scores. /// Whether to apply the game's currently selected mods as a filter when retrieving scores.
/// </summary> /// </summary>
@ -77,6 +95,17 @@ namespace osu.Game.Screens.Select.Leaderboards
if (filterMods) if (filterMods)
UpdateScores(); UpdateScores();
}; };
Content.Add(topScoreContainer = new UserTopScoreContainer
{
ScoreSelected = s => ScoreSelected?.Invoke(s)
});
}
protected override void Reset()
{
base.Reset();
TopScore = null;
} }
protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local;
@ -141,7 +170,11 @@ namespace osu.Game.Screens.Select.Leaderboards
var req = new GetScoresRequest(Beatmap, ruleset.Value ?? Beatmap.Ruleset, Scope, requestMods); var req = new GetScoresRequest(Beatmap, ruleset.Value ?? Beatmap.Ruleset, Scope, requestMods);
req.Success += r => scoresCallback?.Invoke(r.Scores); req.Success += r =>
{
scoresCallback?.Invoke(r.Scores);
TopScore = r.UserScore;
};
return req; return req;
} }

View File

@ -0,0 +1,94 @@
// 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.Threading;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards;
using osu.Game.Scoring;
using osuTK;
namespace osu.Game.Screens.Select.Leaderboards
{
public class UserTopScoreContainer : VisibilityContainer
{
private const int duration = 500;
private readonly Container scoreContainer;
public Bindable<APILegacyUserTopScoreInfo> Score = new Bindable<APILegacyUserTopScoreInfo>();
public Action<ScoreInfo> ScoreSelected;
protected override bool StartHidden => true;
public UserTopScoreContainer()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Margin = new MarginPadding { Vertical = 5 };
Children = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = @"your personal best".ToUpper(),
Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold),
},
scoreContainer = new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
}
}
};
Score.BindValueChanged(onScoreChanged);
}
private CancellationTokenSource loadScoreCancellation;
private void onScoreChanged(ValueChangedEvent<APILegacyUserTopScoreInfo> score)
{
var newScore = score.NewValue;
scoreContainer.Clear();
loadScoreCancellation?.Cancel();
if (newScore == null)
return;
LoadComponentAsync(new LeaderboardScore(newScore.Score, newScore.Position)
{
Action = () => ScoreSelected?.Invoke(newScore.Score)
}, drawableScore =>
{
scoreContainer.Child = drawableScore;
drawableScore.FadeInFromZero(duration, Easing.OutQuint);
}, (loadScoreCancellation = new CancellationTokenSource()).Token);
}
protected override void PopIn() => this.FadeIn(duration, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(duration, Easing.OutQuint);
}
}

View File

@ -223,7 +223,7 @@ namespace osu.Game.Screens.Select
}); });
} }
BeatmapDetails.Leaderboard.ScoreSelected += s => this.Push(new SoloResults(s)); BeatmapDetails.Leaderboard.ScoreSelected += score => this.Push(new SoloResults(score));
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]

View File

@ -13,6 +13,13 @@ namespace osu.Game.Skinning
: base(Info, storage, audioManager, string.Empty) : base(Info, storage, audioManager, string.Empty)
{ {
Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255); Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255);
Configuration.ComboColours.AddRange(new[]
{
new Color4(255, 192, 0, 255),
new Color4(0, 202, 0, 255),
new Color4(18, 124, 255, 255),
new Color4(242, 24, 57, 255),
});
} }
public static SkinInfo Info { get; } = new SkinInfo public static SkinInfo Info { get; } = new SkinInfo

View File

@ -1,4 +1,4 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;

View File

@ -26,10 +26,10 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.913.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.913.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.911.0" /> <PackageReference Include="ppy.osu.Framework" Version="2019.921.0" />
<PackageReference Include="SharpCompress" Version="0.24.0" /> <PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="4.6.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -118,12 +118,12 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.913.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.913.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.911.0" /> <PackageReference Include="ppy.osu.Framework" Version="2019.921.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2019.911.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2019.921.0" />
<PackageReference Include="SharpCompress" Version="0.24.0" /> <PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.11.0" /> <PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="4.6.0" />
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2019.813.0" ExcludeAssets="all" /> <PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2019.813.0" ExcludeAssets="all" />
</ItemGroup> </ItemGroup>
</Project> </Project>