mirror of
https://github.com/ppy/osu.git
synced 2025-02-13 15:53:51 +08:00
Merge branch 'master' into mania-hitwindow-audio-rate-2
This commit is contained in:
commit
8df63ee0f3
@ -50,7 +50,7 @@ Please make sure you have the following prerequisites:
|
|||||||
|
|
||||||
- A desktop platform with the [.NET 6.0 SDK](https://dotnet.microsoft.com/download) installed.
|
- A desktop platform with the [.NET 6.0 SDK](https://dotnet.microsoft.com/download) installed.
|
||||||
|
|
||||||
When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
|
When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Visual Studio Code](https://code.visualstudio.com/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) plugin installed.
|
||||||
|
|
||||||
### Downloading the source code
|
### Downloading the source code
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.817.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.823.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
{
|
{
|
||||||
if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current) != false)
|
if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current, HitResult.Great) == ClickAction.Hit)
|
||||||
{
|
{
|
||||||
// force success
|
// force success
|
||||||
ApplyResult(r => r.Type = HitResult.Great);
|
ApplyResult(r => r.Type = HitResult.Great);
|
||||||
|
@ -13,6 +13,7 @@ using osu.Game.Rulesets.Objects.Types;
|
|||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
private float? alphaAtMiss;
|
private float? alphaAtMiss;
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestHitCircleClassicMod()
|
public void TestHitCircleClassicModMiss()
|
||||||
{
|
{
|
||||||
AddStep("Create hit circle", () =>
|
AddStep("Create hit circle", () =>
|
||||||
{
|
{
|
||||||
@ -61,8 +62,27 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
|
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// No early fade is expected to be applied if the hit circle has been hit.
|
||||||
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
public void TestHitCircleNoMod()
|
public void TestHitCircleClassicModHit()
|
||||||
|
{
|
||||||
|
TestDrawableHitCircle circle = null!;
|
||||||
|
|
||||||
|
AddStep("Create hit circle", () =>
|
||||||
|
{
|
||||||
|
SelectedMods.Value = new Mod[] { new OsuModClassic() };
|
||||||
|
circle = createCircle(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("Wait until circle is hit", () => circle.Result?.Type == HitResult.Great);
|
||||||
|
AddUntilStep("Wait for miss window", () => Clock.CurrentTime, () => Is.GreaterThanOrEqualTo(circle.HitObject.StartTime + circle.HitObject.HitWindows.WindowFor(HitResult.Miss)));
|
||||||
|
AddAssert("Check circle is still visible", () => circle.Alpha, () => Is.GreaterThan(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHitCircleNoModMiss()
|
||||||
{
|
{
|
||||||
AddStep("Create hit circle", () =>
|
AddStep("Create hit circle", () =>
|
||||||
{
|
{
|
||||||
@ -74,6 +94,16 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddAssert("Opaque when missed", () => alphaAtMiss == 1);
|
AddAssert("Opaque when missed", () => alphaAtMiss == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHitCircleNoModHit()
|
||||||
|
{
|
||||||
|
AddStep("Create hit circle", () =>
|
||||||
|
{
|
||||||
|
SelectedMods.Value = Array.Empty<Mod>();
|
||||||
|
createCircle(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSliderClassicMod()
|
public void TestSliderClassicMod()
|
||||||
{
|
{
|
||||||
@ -100,27 +130,32 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddAssert("Head circle opaque when missed", () => alphaAtMiss == 1);
|
AddAssert("Head circle opaque when missed", () => alphaAtMiss == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createCircle()
|
private TestDrawableHitCircle createCircle(bool shouldHit = false)
|
||||||
{
|
{
|
||||||
alphaAtMiss = null;
|
alphaAtMiss = null;
|
||||||
|
|
||||||
DrawableHitCircle drawableHitCircle = new DrawableHitCircle(new HitCircle
|
TestDrawableHitCircle drawableHitCircle = new TestDrawableHitCircle(new HitCircle
|
||||||
{
|
{
|
||||||
StartTime = Time.Current + 500,
|
StartTime = Time.Current + 500,
|
||||||
Position = new Vector2(250)
|
Position = new Vector2(250),
|
||||||
});
|
}, shouldHit);
|
||||||
|
|
||||||
|
drawableHitCircle.Scale = new Vector2(2f);
|
||||||
|
|
||||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
||||||
mod.ApplyToDrawableHitObject(drawableHitCircle);
|
mod.ApplyToDrawableHitObject(drawableHitCircle);
|
||||||
|
|
||||||
drawableHitCircle.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
drawableHitCircle.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
|
|
||||||
drawableHitCircle.OnNewResult += (_, _) =>
|
drawableHitCircle.OnNewResult += (_, result) =>
|
||||||
{
|
{
|
||||||
alphaAtMiss = drawableHitCircle.Alpha;
|
if (!result.IsHit)
|
||||||
|
alphaAtMiss = drawableHitCircle.Alpha;
|
||||||
};
|
};
|
||||||
|
|
||||||
Child = drawableHitCircle;
|
Child = drawableHitCircle;
|
||||||
|
|
||||||
|
return drawableHitCircle;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createSlider()
|
private void createSlider()
|
||||||
@ -138,6 +173,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
drawableSlider.Scale = new Vector2(2f);
|
||||||
|
|
||||||
drawableSlider.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
drawableSlider.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
|
|
||||||
drawableSlider.OnLoadComplete += _ =>
|
drawableSlider.OnLoadComplete += _ =>
|
||||||
@ -145,12 +182,36 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
||||||
mod.ApplyToDrawableHitObject(drawableSlider.HeadCircle);
|
mod.ApplyToDrawableHitObject(drawableSlider.HeadCircle);
|
||||||
|
|
||||||
drawableSlider.HeadCircle.OnNewResult += (_, _) =>
|
drawableSlider.HeadCircle.OnNewResult += (_, result) =>
|
||||||
{
|
{
|
||||||
alphaAtMiss = drawableSlider.HeadCircle.Alpha;
|
if (!result.IsHit)
|
||||||
|
alphaAtMiss = drawableSlider.HeadCircle.Alpha;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
Child = drawableSlider;
|
Child = drawableSlider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected partial class TestDrawableHitCircle : DrawableHitCircle
|
||||||
|
{
|
||||||
|
private readonly bool shouldHit;
|
||||||
|
|
||||||
|
public TestDrawableHitCircle(HitCircle h, bool shouldHit)
|
||||||
|
: base(h)
|
||||||
|
{
|
||||||
|
this.shouldHit = shouldHit;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
|
{
|
||||||
|
if (shouldHit && !userTriggered && timeOffset >= 0)
|
||||||
|
{
|
||||||
|
// force success
|
||||||
|
ApplyResult(r => r.Type = HitResult.Great);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
base.CheckForResult(userTriggered, timeOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,57 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Extensions.TypeExtensions;
|
using osu.Framework.Extensions.TypeExtensions;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Beatmaps.Formats;
|
||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Replays;
|
using osu.Game.Rulesets.Osu.Replays;
|
||||||
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Scoring.Legacy;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Tests
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
{
|
{
|
||||||
public partial class TestSceneObjectOrderedHitPolicy : RateAdjustedBeatmapTestScene
|
public partial class TestSceneLegacyHitPolicy : RateAdjustedBeatmapTestScene
|
||||||
{
|
{
|
||||||
private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss
|
private readonly OsuHitWindows referenceHitWindows;
|
||||||
private const double late_miss_window = 500; // time after +500 is considered a miss
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is provided as a convenience for testing note lock behaviour against osu!stable.
|
||||||
|
/// Setting this field to a non-null path will cause beatmap files and replays used in all test cases
|
||||||
|
/// to be exported to disk so that they can be cross-checked against stable.
|
||||||
|
/// </summary>
|
||||||
|
private readonly string? exportLocation = null;
|
||||||
|
|
||||||
|
public TestSceneLegacyHitPolicy()
|
||||||
|
{
|
||||||
|
referenceHitWindows = new OsuHitWindows();
|
||||||
|
referenceHitWindows.SetDifficulty(0);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged.
|
/// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged.
|
||||||
@ -46,12 +66,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
var hitObjects = new List<OsuHitObject>
|
var hitObjects = new List<OsuHitObject>
|
||||||
{
|
{
|
||||||
new TestHitCircle
|
new HitCircle
|
||||||
{
|
{
|
||||||
StartTime = time_first_circle,
|
StartTime = time_first_circle,
|
||||||
Position = positionFirstCircle
|
Position = positionFirstCircle
|
||||||
},
|
},
|
||||||
new TestHitCircle
|
new HitCircle
|
||||||
{
|
{
|
||||||
StartTime = time_second_circle,
|
StartTime = time_second_circle,
|
||||||
Position = positionSecondCircle
|
Position = positionSecondCircle
|
||||||
@ -65,7 +85,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
||||||
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
||||||
addJudgementOffsetAssert(hitObjects[0], late_miss_window);
|
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
||||||
|
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
||||||
|
addClickActionAssert(0, ClickAction.Shake);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -81,12 +103,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
var hitObjects = new List<OsuHitObject>
|
var hitObjects = new List<OsuHitObject>
|
||||||
{
|
{
|
||||||
new TestHitCircle
|
new HitCircle
|
||||||
{
|
{
|
||||||
StartTime = time_first_circle,
|
StartTime = time_first_circle,
|
||||||
Position = positionFirstCircle
|
Position = positionFirstCircle
|
||||||
},
|
},
|
||||||
new TestHitCircle
|
new HitCircle
|
||||||
{
|
{
|
||||||
StartTime = time_second_circle,
|
StartTime = time_second_circle,
|
||||||
Position = positionSecondCircle
|
Position = positionSecondCircle
|
||||||
@ -100,7 +122,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
||||||
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
||||||
addJudgementOffsetAssert(hitObjects[0], late_miss_window);
|
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
||||||
|
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
||||||
|
addClickActionAssert(0, ClickAction.Shake);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -116,12 +140,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
var hitObjects = new List<OsuHitObject>
|
var hitObjects = new List<OsuHitObject>
|
||||||
{
|
{
|
||||||
new TestHitCircle
|
new HitCircle
|
||||||
{
|
{
|
||||||
StartTime = time_first_circle,
|
StartTime = time_first_circle,
|
||||||
Position = positionFirstCircle
|
Position = positionFirstCircle
|
||||||
},
|
},
|
||||||
new TestHitCircle
|
new HitCircle
|
||||||
{
|
{
|
||||||
StartTime = time_second_circle,
|
StartTime = time_second_circle,
|
||||||
Position = positionSecondCircle
|
Position = positionSecondCircle
|
||||||
@ -135,7 +159,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
||||||
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
||||||
addJudgementOffsetAssert(hitObjects[0], late_miss_window);
|
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
||||||
|
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
||||||
|
addClickActionAssert(0, ClickAction.Shake);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -151,12 +177,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
var hitObjects = new List<OsuHitObject>
|
var hitObjects = new List<OsuHitObject>
|
||||||
{
|
{
|
||||||
new TestHitCircle
|
new HitCircle
|
||||||
{
|
{
|
||||||
StartTime = time_first_circle,
|
StartTime = time_first_circle,
|
||||||
Position = positionFirstCircle
|
Position = positionFirstCircle
|
||||||
},
|
},
|
||||||
new TestHitCircle
|
new HitCircle
|
||||||
{
|
{
|
||||||
StartTime = time_second_circle,
|
StartTime = time_second_circle,
|
||||||
Position = positionSecondCircle
|
Position = positionSecondCircle
|
||||||
@ -165,14 +191,16 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
performTest(hitObjects, new List<ReplayFrame>
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
{
|
{
|
||||||
new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
|
new OsuReplayFrame { Time = time_first_circle - 190, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
|
||||||
new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
|
new OsuReplayFrame { Time = time_first_circle - 90, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
|
||||||
});
|
});
|
||||||
|
|
||||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
addJudgementAssert(hitObjects[0], HitResult.Meh);
|
||||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
||||||
addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200
|
addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
|
||||||
addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100
|
addJudgementOffsetAssert(hitObjects[1], -190); // time_second_circle - first_circle_time - 90
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
|
addClickActionAssert(1, ClickAction.Hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -188,12 +216,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
var hitObjects = new List<OsuHitObject>
|
var hitObjects = new List<OsuHitObject>
|
||||||
{
|
{
|
||||||
new TestHitCircle
|
new HitCircle
|
||||||
{
|
{
|
||||||
StartTime = time_first_circle,
|
StartTime = time_first_circle,
|
||||||
Position = positionFirstCircle
|
Position = positionFirstCircle
|
||||||
},
|
},
|
||||||
new TestHitCircle
|
new HitCircle
|
||||||
{
|
{
|
||||||
StartTime = time_second_circle,
|
StartTime = time_second_circle,
|
||||||
Position = positionSecondCircle
|
Position = positionSecondCircle
|
||||||
@ -202,21 +230,23 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
performTest(hitObjects, new List<ReplayFrame>
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
{
|
{
|
||||||
new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
|
new OsuReplayFrame { Time = time_first_circle - 190, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
|
||||||
new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
|
new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
|
||||||
});
|
});
|
||||||
|
|
||||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
addJudgementAssert(hitObjects[0], HitResult.Meh);
|
||||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
addJudgementAssert(hitObjects[1], HitResult.Ok);
|
||||||
addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200
|
addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
|
||||||
addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time
|
addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
|
addClickActionAssert(1, ClickAction.Hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tests clicking a future circle after a slider's start time, but hitting all slider ticks.
|
/// Tests clicking a future circle after a slider's start time, but hitting the slider head and all slider ticks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
public void TestMissSliderHeadAndHitAllSliderTicks()
|
public void TestHitCircleBeforeSliderHead()
|
||||||
{
|
{
|
||||||
const double time_slider = 1500;
|
const double time_slider = 1500;
|
||||||
const double time_circle = 1510;
|
const double time_circle = 1510;
|
||||||
@ -225,19 +255,19 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
var hitObjects = new List<OsuHitObject>
|
var hitObjects = new List<OsuHitObject>
|
||||||
{
|
{
|
||||||
new TestHitCircle
|
new HitCircle
|
||||||
{
|
{
|
||||||
StartTime = time_circle,
|
StartTime = time_circle,
|
||||||
Position = positionCircle
|
Position = positionCircle
|
||||||
},
|
},
|
||||||
new TestSlider
|
new Slider
|
||||||
{
|
{
|
||||||
StartTime = time_slider,
|
StartTime = time_slider,
|
||||||
Position = positionSlider,
|
Position = positionSlider,
|
||||||
Path = new SliderPath(PathType.Linear, new[]
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
{
|
{
|
||||||
Vector2.Zero,
|
Vector2.Zero,
|
||||||
new Vector2(25, 0),
|
new Vector2(50, 0),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -248,10 +278,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } }
|
new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } }
|
||||||
});
|
});
|
||||||
|
|
||||||
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||||
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
|
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
|
||||||
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
|
addClickActionAssert(1, ClickAction.Hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -267,19 +299,19 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
var hitObjects = new List<OsuHitObject>
|
var hitObjects = new List<OsuHitObject>
|
||||||
{
|
{
|
||||||
new TestHitCircle
|
new HitCircle
|
||||||
{
|
{
|
||||||
StartTime = time_circle,
|
StartTime = time_circle,
|
||||||
Position = positionCircle
|
Position = positionCircle
|
||||||
},
|
},
|
||||||
new TestSlider
|
new Slider
|
||||||
{
|
{
|
||||||
StartTime = time_slider,
|
StartTime = time_slider,
|
||||||
Position = positionSlider,
|
Position = positionSlider,
|
||||||
Path = new SliderPath(PathType.Linear, new[]
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
{
|
{
|
||||||
Vector2.Zero,
|
Vector2.Zero,
|
||||||
new Vector2(25, 0),
|
new Vector2(50, 0),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -287,14 +319,16 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
performTest(hitObjects, new List<ReplayFrame>
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
{
|
{
|
||||||
new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
|
new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
|
||||||
new OsuReplayFrame { Time = time_circle + late_miss_window - 100, Position = positionCircle, Actions = { OsuAction.RightButton } },
|
new OsuReplayFrame { Time = time_circle + referenceHitWindows.WindowFor(HitResult.Meh) - 100, Position = positionCircle, Actions = { OsuAction.RightButton } },
|
||||||
new OsuReplayFrame { Time = time_circle + late_miss_window - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } },
|
new OsuReplayFrame { Time = time_circle + referenceHitWindows.WindowFor(HitResult.Meh) - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } },
|
||||||
});
|
});
|
||||||
|
|
||||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
addJudgementAssert(hitObjects[0], HitResult.Ok);
|
||||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||||
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
|
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
|
||||||
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
|
addClickActionAssert(1, ClickAction.Hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -304,7 +338,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
public void TestHitCircleBeforeSpinner()
|
public void TestHitCircleBeforeSpinner()
|
||||||
{
|
{
|
||||||
const double time_spinner = 1500;
|
const double time_spinner = 1500;
|
||||||
const double time_circle = 1800;
|
const double time_circle = 1600;
|
||||||
Vector2 positionCircle = Vector2.Zero;
|
Vector2 positionCircle = Vector2.Zero;
|
||||||
|
|
||||||
var hitObjects = new List<OsuHitObject>
|
var hitObjects = new List<OsuHitObject>
|
||||||
@ -315,7 +349,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
Position = new Vector2(256, 192),
|
Position = new Vector2(256, 192),
|
||||||
EndTime = time_spinner + 1000,
|
EndTime = time_spinner + 1000,
|
||||||
},
|
},
|
||||||
new TestHitCircle
|
new HitCircle
|
||||||
{
|
{
|
||||||
StartTime = time_circle,
|
StartTime = time_circle,
|
||||||
Position = positionCircle
|
Position = positionCircle
|
||||||
@ -324,7 +358,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
performTest(hitObjects, new List<ReplayFrame>
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
{
|
{
|
||||||
new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } },
|
new OsuReplayFrame { Time = time_spinner - 90, Position = positionCircle, Actions = { OsuAction.LeftButton } },
|
||||||
new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
|
new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
|
||||||
new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } },
|
new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } },
|
||||||
new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } },
|
new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } },
|
||||||
@ -333,7 +367,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
});
|
});
|
||||||
|
|
||||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -346,12 +381,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
var hitObjects = new List<OsuHitObject>
|
var hitObjects = new List<OsuHitObject>
|
||||||
{
|
{
|
||||||
new TestHitCircle
|
new HitCircle
|
||||||
{
|
{
|
||||||
StartTime = time_circle,
|
StartTime = time_circle,
|
||||||
Position = positionCircle
|
Position = positionCircle
|
||||||
},
|
},
|
||||||
new TestSlider
|
new Slider
|
||||||
{
|
{
|
||||||
StartTime = time_slider,
|
StartTime = time_slider,
|
||||||
Position = positionSlider,
|
Position = positionSlider,
|
||||||
@ -372,6 +407,102 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
addJudgementAssert(hitObjects[0], HitResult.Great);
|
addJudgementAssert(hitObjects[0], HitResult.Great);
|
||||||
addJudgementAssert(hitObjects[1], HitResult.Great);
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||||
|
addClickActionAssert(0, ClickAction.Shake);
|
||||||
|
addClickActionAssert(1, ClickAction.Hit);
|
||||||
|
addClickActionAssert(2, ClickAction.Hit);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOverlappingSliders()
|
||||||
|
{
|
||||||
|
const double time_first_slider = 1000;
|
||||||
|
const double time_second_slider = 1200;
|
||||||
|
Vector2 positionFirstSlider = new Vector2(100, 50);
|
||||||
|
Vector2 positionSecondSlider = new Vector2(100, 80);
|
||||||
|
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
|
||||||
|
|
||||||
|
var hitObjects = new List<OsuHitObject>
|
||||||
|
{
|
||||||
|
new Slider
|
||||||
|
{
|
||||||
|
StartTime = time_first_slider,
|
||||||
|
Position = positionFirstSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
new Slider
|
||||||
|
{
|
||||||
|
StartTime = time_second_slider,
|
||||||
|
Position = positionSecondSlider,
|
||||||
|
Path = new SliderPath(PathType.Linear, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(25, 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint, Actions = { OsuAction.LeftButton, OsuAction.RightButton } },
|
||||||
|
new OsuReplayFrame { Time = time_first_slider + 50, Position = midpoint },
|
||||||
|
new OsuReplayFrame { Time = time_second_slider, Position = positionSecondSlider + new Vector2(0, 10), Actions = { OsuAction.LeftButton } },
|
||||||
|
});
|
||||||
|
|
||||||
|
addJudgementAssert(hitObjects[0], HitResult.Ok);
|
||||||
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
||||||
|
addClickActionAssert(0, ClickAction.Hit);
|
||||||
|
addClickActionAssert(1, ClickAction.Hit);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestStacksDoNotShake()
|
||||||
|
{
|
||||||
|
const double time_stack_start = 1000;
|
||||||
|
Vector2 position = new Vector2(80);
|
||||||
|
|
||||||
|
var hitObjects = Enumerable.Range(0, 20).Select(i => new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = time_stack_start + i * 100,
|
||||||
|
Position = position
|
||||||
|
}).Cast<OsuHitObject>().ToList();
|
||||||
|
|
||||||
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Time = time_stack_start - 450, Position = new Vector2(55), Actions = { OsuAction.LeftButton } },
|
||||||
|
});
|
||||||
|
|
||||||
|
addClickActionAssert(0, ClickAction.Ignore);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAutopilotReducesHittableRange()
|
||||||
|
{
|
||||||
|
const double time_circle = 1500;
|
||||||
|
Vector2 positionCircle = Vector2.Zero;
|
||||||
|
|
||||||
|
var hitObjects = new List<OsuHitObject>
|
||||||
|
{
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = time_circle,
|
||||||
|
Position = positionCircle
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
performTest(hitObjects, new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame { Time = time_circle - 250, Position = positionCircle, Actions = { OsuAction.LeftButton } }
|
||||||
|
}, new Mod[] { new OsuModAutopilot() });
|
||||||
|
|
||||||
|
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
||||||
|
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
||||||
|
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
||||||
|
addClickActionAssert(0, ClickAction.Shake);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
||||||
@ -380,38 +511,119 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
() => judgementResults.Single(r => r.HitObject == hitObject).Type, () => Is.EqualTo(result));
|
() => judgementResults.Single(r => r.HitObject == hitObject).Type, () => Is.EqualTo(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addJudgementAssert(string name, Func<OsuHitObject> hitObject, HitResult result)
|
private void addJudgementAssert(string name, Func<OsuHitObject?> hitObject, HitResult result)
|
||||||
{
|
{
|
||||||
AddAssert($"{name} judgement is {result}",
|
AddAssert($"{name} judgement is {result}",
|
||||||
() => judgementResults.Single(r => r.HitObject == hitObject()).Type == result);
|
() => judgementResults.Single(r => r.HitObject == hitObject()).Type, () => Is.EqualTo(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset)
|
private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset)
|
||||||
{
|
{
|
||||||
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
|
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
|
||||||
() => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
|
() => judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, () => Is.EqualTo(offset).Within(50));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ScoreAccessibleReplayPlayer currentPlayer;
|
private void addClickActionAssert(int inputIndex, ClickAction action)
|
||||||
private List<JudgementResult> judgementResults;
|
=> AddAssert($"input #{inputIndex} resulted in {action}", () => testPolicy.ClickActions[inputIndex], () => Is.EqualTo(action));
|
||||||
|
|
||||||
private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames)
|
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
||||||
|
private List<JudgementResult> judgementResults = null!;
|
||||||
|
private TestLegacyHitPolicy testPolicy = null!;
|
||||||
|
|
||||||
|
private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames, IEnumerable<Mod>? extraMods = null, [CallerMemberName] string testCaseName = "")
|
||||||
{
|
{
|
||||||
AddStep("load player", () =>
|
List<Mod> mods = null!;
|
||||||
|
IBeatmap playableBeatmap = null!;
|
||||||
|
Score score = null!;
|
||||||
|
|
||||||
|
AddStep("set up mods", () =>
|
||||||
{
|
{
|
||||||
|
mods = new List<Mod> { new OsuModClassic() };
|
||||||
|
|
||||||
|
if (extraMods != null)
|
||||||
|
mods.AddRange(extraMods);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("create beatmap", () =>
|
||||||
|
{
|
||||||
|
var cpi = new ControlPointInfo();
|
||||||
|
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
|
||||||
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
|
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
|
||||||
{
|
{
|
||||||
|
Metadata =
|
||||||
|
{
|
||||||
|
Title = testCaseName
|
||||||
|
},
|
||||||
HitObjects = hitObjects,
|
HitObjects = hitObjects,
|
||||||
Difficulty = new BeatmapDifficulty { SliderTickRate = 3 },
|
Difficulty = new BeatmapDifficulty
|
||||||
|
{
|
||||||
|
OverallDifficulty = 0,
|
||||||
|
SliderTickRate = 3
|
||||||
|
},
|
||||||
BeatmapInfo =
|
BeatmapInfo =
|
||||||
{
|
{
|
||||||
Ruleset = new OsuRuleset().RulesetInfo
|
Ruleset = new OsuRuleset().RulesetInfo,
|
||||||
|
BeatmapVersion = LegacyBeatmapEncoder.FIRST_LAZER_VERSION // for correct offset treatment by score encoder
|
||||||
},
|
},
|
||||||
|
ControlPointInfo = cpi
|
||||||
|
});
|
||||||
|
playableBeatmap = Beatmap.Value.GetPlayableBeatmap(new OsuRuleset().RulesetInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("create score", () =>
|
||||||
|
{
|
||||||
|
score = new Score
|
||||||
|
{
|
||||||
|
Replay = new Replay
|
||||||
|
{
|
||||||
|
Frames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
// required for correct playback in stable
|
||||||
|
new OsuReplayFrame(0, new Vector2(256, -500)),
|
||||||
|
new OsuReplayFrame(0, new Vector2(256, -500))
|
||||||
|
}.Concat(frames).ToList()
|
||||||
|
},
|
||||||
|
ScoreInfo =
|
||||||
|
{
|
||||||
|
Ruleset = new OsuRuleset().RulesetInfo,
|
||||||
|
BeatmapInfo = playableBeatmap.BeatmapInfo,
|
||||||
|
Mods = mods.ToArray()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exportLocation != null)
|
||||||
|
{
|
||||||
|
AddStep("export beatmap", () =>
|
||||||
|
{
|
||||||
|
var beatmapEncoder = new LegacyBeatmapEncoder(playableBeatmap, null);
|
||||||
|
|
||||||
|
using (var stream = File.Open(Path.Combine(exportLocation, $"{testCaseName}.osu"), FileMode.Create))
|
||||||
|
{
|
||||||
|
var memoryStream = new MemoryStream();
|
||||||
|
using (var writer = new StreamWriter(memoryStream, Encoding.UTF8, leaveOpen: true))
|
||||||
|
beatmapEncoder.Encode(writer);
|
||||||
|
|
||||||
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
memoryStream.CopyTo(stream);
|
||||||
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
playableBeatmap.BeatmapInfo.MD5Hash = memoryStream.ComputeMD5Hash();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
SelectedMods.Value = new[] { new OsuModClassic() };
|
AddStep("export score", () =>
|
||||||
|
{
|
||||||
|
using var stream = File.Open(Path.Combine(exportLocation, $"{testCaseName}.osr"), FileMode.Create);
|
||||||
|
var encoder = new LegacyScoreEncoder(score, playableBeatmap);
|
||||||
|
encoder.Encode(stream);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
AddStep("load player", () =>
|
||||||
|
{
|
||||||
|
SelectedMods.Value = mods.ToArray();
|
||||||
|
|
||||||
|
var p = new ScoreAccessibleReplayPlayer(score);
|
||||||
|
|
||||||
p.OnLoadComplete += _ =>
|
p.OnLoadComplete += _ =>
|
||||||
{
|
{
|
||||||
@ -427,29 +639,13 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
AddStep("Substitute hit policy", () =>
|
||||||
}
|
|
||||||
|
|
||||||
private class TestHitCircle : HitCircle
|
|
||||||
{
|
|
||||||
protected override HitWindows CreateHitWindows() => new TestHitWindows();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TestSlider : Slider
|
|
||||||
{
|
|
||||||
public TestSlider()
|
|
||||||
{
|
{
|
||||||
SliderVelocity = 0.1f;
|
var playfield = currentPlayer.ChildrenOfType<OsuPlayfield>().Single();
|
||||||
|
var currentPolicy = playfield.HitPolicy;
|
||||||
DefaultsApplied += _ =>
|
playfield.HitPolicy = testPolicy = new TestLegacyHitPolicy(currentPolicy);
|
||||||
{
|
});
|
||||||
HeadCircle.HitWindows = new TestHitWindows();
|
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||||
TailCircle.HitWindows = new TestHitWindows();
|
|
||||||
|
|
||||||
HeadCircle.HitWindows.SetDifficulty(0);
|
|
||||||
TailCircle.HitWindows.SetDifficulty(0);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestSpinner : Spinner
|
private class TestSpinner : Spinner
|
||||||
@ -461,19 +657,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestHitWindows : HitWindows
|
|
||||||
{
|
|
||||||
private static readonly DifficultyRange[] ranges =
|
|
||||||
{
|
|
||||||
new DifficultyRange(HitResult.Great, 500, 500, 500),
|
|
||||||
new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window),
|
|
||||||
};
|
|
||||||
|
|
||||||
public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss;
|
|
||||||
|
|
||||||
protected override DifficultyRange[] GetRanges() => ranges;
|
|
||||||
}
|
|
||||||
|
|
||||||
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
|
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||||
{
|
{
|
||||||
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
||||||
@ -489,5 +672,24 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class TestLegacyHitPolicy : LegacyHitPolicy
|
||||||
|
{
|
||||||
|
private readonly IHitPolicy currentPolicy;
|
||||||
|
|
||||||
|
public TestLegacyHitPolicy(IHitPolicy currentPolicy)
|
||||||
|
{
|
||||||
|
this.currentPolicy = currentPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ClickAction> ClickActions { get; } = new List<ClickAction>();
|
||||||
|
|
||||||
|
public override ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result)
|
||||||
|
{
|
||||||
|
var action = currentPolicy.CheckHittable(hitObject, time, result);
|
||||||
|
ClickActions.Add(action);
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -11,6 +11,7 @@ using osu.Game.Rulesets.Objects;
|
|||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
@ -57,7 +58,10 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
var osuRuleset = (DrawableOsuRuleset)drawableRuleset;
|
var osuRuleset = (DrawableOsuRuleset)drawableRuleset;
|
||||||
|
|
||||||
if (ClassicNoteLock.Value)
|
if (ClassicNoteLock.Value)
|
||||||
osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy();
|
{
|
||||||
|
double hittableRange = OsuHitWindows.MISS_WINDOW - (drawableRuleset.Mods.OfType<OsuModAutopilot>().Any() ? 200 : 0);
|
||||||
|
osuRuleset.Playfield.HitPolicy = new LegacyHitPolicy(hittableRange);
|
||||||
|
}
|
||||||
|
|
||||||
usingHiddenFading = drawableRuleset.Mods.OfType<OsuModHidden>().SingleOrDefault()?.OnlyFadeApproachCircles.Value == false;
|
usingHiddenFading = drawableRuleset.Mods.OfType<OsuModHidden>().SingleOrDefault()?.OnlyFadeApproachCircles.Value == false;
|
||||||
}
|
}
|
||||||
@ -85,13 +89,16 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
private void applyEarlyFading(DrawableHitCircle circle)
|
private void applyEarlyFading(DrawableHitCircle circle)
|
||||||
{
|
{
|
||||||
circle.ApplyCustomUpdateState += (o, _) =>
|
circle.ApplyCustomUpdateState += (dho, state) =>
|
||||||
{
|
{
|
||||||
using (o.BeginAbsoluteSequence(o.StateUpdateTime))
|
using (dho.BeginAbsoluteSequence(dho.StateUpdateTime))
|
||||||
{
|
{
|
||||||
double okWindow = o.HitObject.HitWindows.WindowFor(HitResult.Ok);
|
if (state != ArmedState.Hit)
|
||||||
double lateMissFadeTime = o.HitObject.HitWindows.WindowFor(HitResult.Meh) - okWindow;
|
{
|
||||||
o.Delay(okWindow).FadeOut(lateMissFadeTime);
|
double okWindow = dho.HitObject.HitWindows.WindowFor(HitResult.Ok);
|
||||||
|
double lateMissFadeTime = dho.HitObject.HitWindows.WindowFor(HitResult.Meh) - okWindow;
|
||||||
|
dho.Delay(okWindow).FadeOut(lateMissFadeTime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -68,8 +68,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
public void OnSliderTrackingChange(ValueChangedEvent<bool> e)
|
public void OnSliderTrackingChange(ValueChangedEvent<bool> e)
|
||||||
{
|
{
|
||||||
// If a slider is in a tracking state, a further dim should be applied to the (remaining) visible portion of the playfield over a brief duration.
|
// If a slider is in a tracking state, a further dim should be applied to the (remaining) visible portion of the playfield.
|
||||||
this.TransformTo(nameof(FlashlightDim), e.NewValue ? 0.8f : 0.0f, 50);
|
FlashlightDim = e.NewValue ? 0.8f : 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||||
|
@ -18,6 +18,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
|||||||
using osu.Game.Rulesets.Osu.Judgements;
|
using osu.Game.Rulesets.Osu.Judgements;
|
||||||
using osu.Game.Rulesets.Osu.Skinning;
|
using osu.Game.Rulesets.Osu.Skinning;
|
||||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -154,12 +155,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
}
|
}
|
||||||
|
|
||||||
var result = ResultFor(timeOffset);
|
var result = ResultFor(timeOffset);
|
||||||
|
var clickAction = CheckHittable?.Invoke(this, Time.Current, result);
|
||||||
|
|
||||||
if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false)
|
if (clickAction == ClickAction.Shake)
|
||||||
{
|
|
||||||
Shake();
|
Shake();
|
||||||
|
|
||||||
|
if (result == HitResult.None || clickAction != ClickAction.Hit)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
ApplyResult(r =>
|
ApplyResult(r =>
|
||||||
{
|
{
|
||||||
|
@ -12,6 +12,8 @@ using osu.Game.Rulesets.Judgements;
|
|||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Judgements;
|
using osu.Game.Rulesets.Osu.Judgements;
|
||||||
using osu.Game.Rulesets.Osu.Scoring;
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -30,10 +32,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
protected override float SamplePlaybackPosition => CalculateDrawableRelativePosition(this);
|
protected override float SamplePlaybackPosition => CalculateDrawableRelativePosition(this);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this <see cref="DrawableOsuHitObject"/> can be hit, given a time value.
|
/// What action this <see cref="DrawableOsuHitObject"/> should take in response to a
|
||||||
/// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false.
|
/// click at the given time value.
|
||||||
|
/// If non-null, judgements will be ignored for return values of <see cref="ClickAction.Ignore"/>
|
||||||
|
/// and <see cref="ClickAction.Shake"/>, and this hit object will be shaken for return values of
|
||||||
|
/// <see cref="ClickAction.Shake"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<DrawableHitObject, double, bool> CheckHittable;
|
public Func<DrawableHitObject, double, HitResult, ClickAction> CheckHittable;
|
||||||
|
|
||||||
protected DrawableOsuHitObject(OsuHitObject hitObject)
|
protected DrawableOsuHitObject(OsuHitObject hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
|
@ -8,6 +8,7 @@ using System.Diagnostics;
|
|||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
@ -60,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
pathVersion.BindTo(DrawableSlider.PathVersion);
|
pathVersion.BindTo(DrawableSlider.PathVersion);
|
||||||
|
|
||||||
CheckHittable = (d, t) => DrawableSlider.CheckHittable?.Invoke(d, t) ?? true;
|
CheckHittable = (d, t, r) => DrawableSlider.CheckHittable?.Invoke(d, t, r) ?? ClickAction.Hit;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.UI
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
@ -13,9 +12,9 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class AnyOrderHitPolicy : IHitPolicy
|
public class AnyOrderHitPolicy : IHitPolicy
|
||||||
{
|
{
|
||||||
public IHitObjectContainer HitObjectContainer { get; set; }
|
public IHitObjectContainer HitObjectContainer { get; set; } = null!;
|
||||||
|
|
||||||
public bool IsHittable(DrawableHitObject hitObject, double time) => true;
|
public ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result) => ClickAction.Hit;
|
||||||
|
|
||||||
public void HandleHit(DrawableHitObject hitObject)
|
public void HandleHit(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
|
18
osu.Game.Rulesets.Osu/UI/ClickAction.cs
Normal file
18
osu.Game.Rulesets.Osu/UI/ClickAction.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An action that an <see cref="IHitPolicy"/> recommends be taken in response to a click
|
||||||
|
/// on a <see cref="DrawableOsuHitObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
public enum ClickAction
|
||||||
|
{
|
||||||
|
Ignore,
|
||||||
|
Shake,
|
||||||
|
Hit
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.UI
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
@ -19,8 +20,9 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to check.</param>
|
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to check.</param>
|
||||||
/// <param name="time">The time to check.</param>
|
/// <param name="time">The time to check.</param>
|
||||||
|
/// <param name="result">The result that the object would be judged with if hit.</param>
|
||||||
/// <returns>Whether <paramref name="hitObject"/> can be hit at the given <paramref name="time"/>.</returns>
|
/// <returns>Whether <paramref name="hitObject"/> can be hit at the given <paramref name="time"/>.</returns>
|
||||||
bool IsHittable(DrawableHitObject hitObject, double time);
|
ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles a <see cref="HitObject"/> being hit.
|
/// Handles a <see cref="HitObject"/> being hit.
|
||||||
|
72
osu.Game.Rulesets.Osu/UI/LegacyHitPolicy.cs
Normal file
72
osu.Game.Rulesets.Osu/UI/LegacyHitPolicy.cs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
// 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.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures that <see cref="HitObject"/>s are hit in order of appearance. The classic note lock.
|
||||||
|
/// <remarks>
|
||||||
|
/// Hits will be blocked until the previous <see cref="HitObject"/>s have been judged.
|
||||||
|
/// </remarks>
|
||||||
|
/// </summary>
|
||||||
|
public class LegacyHitPolicy : IHitPolicy
|
||||||
|
{
|
||||||
|
public IHitObjectContainer? HitObjectContainer { get; set; }
|
||||||
|
|
||||||
|
private readonly double hittableRange;
|
||||||
|
|
||||||
|
public LegacyHitPolicy(double hittableRange = OsuHitWindows.MISS_WINDOW)
|
||||||
|
{
|
||||||
|
this.hittableRange = hittableRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HandleHit(DrawableHitObject hitObject)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result)
|
||||||
|
{
|
||||||
|
if (HitObjectContainer == null)
|
||||||
|
throw new InvalidOperationException($"{nameof(HitObjectContainer)} should be set before {nameof(CheckHittable)} is called.");
|
||||||
|
|
||||||
|
var aliveObjects = HitObjectContainer.AliveObjects.ToList();
|
||||||
|
int index = aliveObjects.IndexOf(hitObject);
|
||||||
|
|
||||||
|
if (index > 0)
|
||||||
|
{
|
||||||
|
var previousHitObject = (DrawableOsuHitObject)aliveObjects[index - 1];
|
||||||
|
if (previousHitObject.HitObject.StackHeight > 0 && !previousHitObject.AllJudged)
|
||||||
|
return ClickAction.Ignore;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == HitResult.None)
|
||||||
|
return ClickAction.Shake;
|
||||||
|
|
||||||
|
foreach (DrawableHitObject testObject in aliveObjects)
|
||||||
|
{
|
||||||
|
if (testObject.AllJudged)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// if we found the object being checked, we can move on to the final timing test.
|
||||||
|
if (testObject == hitObject)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// for all other objects, we check for validity and block the hit if any are still valid.
|
||||||
|
// 3ms of extra leniency to account for slightly unsnapped objects.
|
||||||
|
if (testObject.HitObject.GetEndTime() + 3 < hitObject.HitObject.StartTime)
|
||||||
|
return ClickAction.Shake;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.Abs(hitObject.HitObject.StartTime - time) < hittableRange ? ClickAction.Hit : ClickAction.Shake;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,56 +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.
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Game.Rulesets.Objects;
|
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
|
||||||
using osu.Game.Rulesets.UI;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.UI
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Ensures that <see cref="HitObject"/>s are hit in order of appearance. The classic note lock.
|
|
||||||
/// <remarks>
|
|
||||||
/// Hits will be blocked until the previous <see cref="HitObject"/>s have been judged.
|
|
||||||
/// </remarks>
|
|
||||||
/// </summary>
|
|
||||||
public class ObjectOrderedHitPolicy : IHitPolicy
|
|
||||||
{
|
|
||||||
public IHitObjectContainer HitObjectContainer { get; set; }
|
|
||||||
|
|
||||||
public bool IsHittable(DrawableHitObject hitObject, double time) => enumerateHitObjectsUpTo(hitObject.HitObject.StartTime).All(obj => obj.AllJudged);
|
|
||||||
|
|
||||||
public void HandleHit(DrawableHitObject hitObject)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<DrawableHitObject> enumerateHitObjectsUpTo(double targetTime)
|
|
||||||
{
|
|
||||||
foreach (var obj in HitObjectContainer.AliveObjects)
|
|
||||||
{
|
|
||||||
if (obj.HitObject.StartTime >= targetTime)
|
|
||||||
yield break;
|
|
||||||
|
|
||||||
switch (obj)
|
|
||||||
{
|
|
||||||
case DrawableSpinner:
|
|
||||||
continue;
|
|
||||||
|
|
||||||
case DrawableSlider slider:
|
|
||||||
yield return slider.HeadCircle;
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
yield return obj;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
|
|
||||||
protected override void OnNewDrawableHitObject(DrawableHitObject drawable)
|
protected override void OnNewDrawableHitObject(DrawableHitObject drawable)
|
||||||
{
|
{
|
||||||
((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable;
|
((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.CheckHittable;
|
||||||
|
|
||||||
Debug.Assert(!drawable.IsLoaded, $"Already loaded {nameof(DrawableHitObject)} is added to {nameof(OsuPlayfield)}");
|
Debug.Assert(!drawable.IsLoaded, $"Already loaded {nameof(DrawableHitObject)} is added to {nameof(OsuPlayfield)}");
|
||||||
drawable.OnLoadComplete += onDrawableHitObjectLoaded;
|
drawable.OnLoadComplete += onDrawableHitObjectLoaded;
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.UI
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
@ -22,11 +21,14 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class StartTimeOrderedHitPolicy : IHitPolicy
|
public class StartTimeOrderedHitPolicy : IHitPolicy
|
||||||
{
|
{
|
||||||
public IHitObjectContainer HitObjectContainer { get; set; }
|
public IHitObjectContainer? HitObjectContainer { get; set; }
|
||||||
|
|
||||||
public bool IsHittable(DrawableHitObject hitObject, double time)
|
public ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult _)
|
||||||
{
|
{
|
||||||
DrawableHitObject blockingObject = null;
|
if (HitObjectContainer == null)
|
||||||
|
throw new InvalidOperationException($"{nameof(HitObjectContainer)} should be set before {nameof(CheckHittable)} is called.");
|
||||||
|
|
||||||
|
DrawableHitObject? blockingObject = null;
|
||||||
|
|
||||||
foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
|
foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
|
||||||
{
|
{
|
||||||
@ -36,22 +38,25 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
|
|
||||||
// If there is no previous hitobject, allow the hit.
|
// If there is no previous hitobject, allow the hit.
|
||||||
if (blockingObject == null)
|
if (blockingObject == null)
|
||||||
return true;
|
return ClickAction.Hit;
|
||||||
|
|
||||||
// A hit is allowed if:
|
// A hit is allowed if:
|
||||||
// 1. The last blocking hitobject has been judged.
|
// 1. The last blocking hitobject has been judged.
|
||||||
// 2. The current time is after the last hitobject's start time.
|
// 2. The current time is after the last hitobject's start time.
|
||||||
// Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245).
|
// Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245).
|
||||||
return blockingObject.Judged || time >= blockingObject.HitObject.StartTime;
|
return (blockingObject.Judged || time >= blockingObject.HitObject.StartTime) ? ClickAction.Hit : ClickAction.Shake;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void HandleHit(DrawableHitObject hitObject)
|
public void HandleHit(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
|
if (HitObjectContainer == null)
|
||||||
|
throw new InvalidOperationException($"{nameof(HitObjectContainer)} should be set before {nameof(HandleHit)} is called.");
|
||||||
|
|
||||||
// Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners).
|
// Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners).
|
||||||
if (!hitObjectCanBlockFutureHits(hitObject))
|
if (!hitObjectCanBlockFutureHits(hitObject))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset))
|
if (CheckHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset, hitObject.Result.Type) != ClickAction.Hit)
|
||||||
throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!");
|
throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!");
|
||||||
|
|
||||||
// Miss all hitobjects prior to the hit one.
|
// Miss all hitobjects prior to the hit one.
|
||||||
@ -74,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
|
|
||||||
private IEnumerable<DrawableHitObject> enumerateHitObjectsUpTo(double targetTime)
|
private IEnumerable<DrawableHitObject> enumerateHitObjectsUpTo(double targetTime)
|
||||||
{
|
{
|
||||||
foreach (var obj in HitObjectContainer.AliveObjects)
|
foreach (var obj in HitObjectContainer!.AliveObjects)
|
||||||
{
|
{
|
||||||
if (obj.HitObject.StartTime >= targetTime)
|
if (obj.HitObject.StartTime >= targetTime)
|
||||||
yield break;
|
yield break;
|
||||||
|
@ -9,6 +9,7 @@ using osu.Framework.Extensions.ObjectExtensions;
|
|||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Rulesets.Mania;
|
using osu.Game.Rulesets.Mania;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
@ -16,6 +17,7 @@ using osu.Game.Screens.Edit;
|
|||||||
using osu.Game.Screens.Edit.GameplayTest;
|
using osu.Game.Screens.Edit.GameplayTest;
|
||||||
using osu.Game.Screens.Menu;
|
using osu.Game.Screens.Menu;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
|
using osu.Game.Screens.Select.Filter;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
@ -203,6 +205,33 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying);
|
AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase(SortMode.Title)]
|
||||||
|
[TestCase(SortMode.Difficulty)]
|
||||||
|
public void TestSelectionRetainedOnExit(SortMode sortMode)
|
||||||
|
{
|
||||||
|
BeatmapSetInfo beatmapSet = null!;
|
||||||
|
|
||||||
|
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
||||||
|
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
||||||
|
|
||||||
|
AddStep($"set sort mode to {sortMode}", () => Game.LocalConfig.SetValue(OsuSetting.SongSelectSortingMode, sortMode));
|
||||||
|
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||||
|
AddUntilStep("wait for song select",
|
||||||
|
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||||
|
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||||
|
&& songSelect.IsLoaded);
|
||||||
|
|
||||||
|
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||||
|
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||||
|
|
||||||
|
AddStep("exit editor", () => InputManager.Key(Key.Escape));
|
||||||
|
AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor);
|
||||||
|
|
||||||
|
AddUntilStep("selection retained on song select",
|
||||||
|
() => Game.Beatmap.Value.BeatmapInfo.ID,
|
||||||
|
() => Is.EqualTo(beatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0).ID));
|
||||||
|
}
|
||||||
|
|
||||||
private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType<EditorBeatmap>().Single();
|
private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType<EditorBeatmap>().Single();
|
||||||
|
|
||||||
private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen;
|
private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen;
|
||||||
|
@ -39,6 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
private BeatmapInfo currentSelection => carousel.SelectedBeatmapInfo;
|
private BeatmapInfo currentSelection => carousel.SelectedBeatmapInfo;
|
||||||
|
|
||||||
private const int set_count = 5;
|
private const int set_count = 5;
|
||||||
|
private const int diff_count = 3;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(RulesetStore rulesets)
|
private void load(RulesetStore rulesets)
|
||||||
@ -111,7 +112,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestScrollPositionMaintainedOnAdd()
|
public void TestScrollPositionMaintainedOnAdd()
|
||||||
{
|
{
|
||||||
loadBeatmaps(count: 1, randomDifficulties: false);
|
loadBeatmaps(setCount: 1);
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++)
|
for (int i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
@ -124,7 +125,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestDeletion()
|
public void TestDeletion()
|
||||||
{
|
{
|
||||||
loadBeatmaps(count: 5, randomDifficulties: true);
|
loadBeatmaps(setCount: 5, randomDifficulties: true);
|
||||||
|
|
||||||
AddStep("remove first set", () => carousel.RemoveBeatmapSet(carousel.Items.Select(item => item.Item).OfType<CarouselBeatmapSet>().First().BeatmapSet));
|
AddStep("remove first set", () => carousel.RemoveBeatmapSet(carousel.Items.Select(item => item.Item).OfType<CarouselBeatmapSet>().First().BeatmapSet));
|
||||||
AddUntilStep("4 beatmap sets visible", () => this.ChildrenOfType<DrawableCarouselBeatmapSet>().Count(set => set.Alpha > 0) == 4);
|
AddUntilStep("4 beatmap sets visible", () => this.ChildrenOfType<DrawableCarouselBeatmapSet>().Count(set => set.Alpha > 0) == 4);
|
||||||
@ -133,7 +134,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestScrollPositionMaintainedOnDelete()
|
public void TestScrollPositionMaintainedOnDelete()
|
||||||
{
|
{
|
||||||
loadBeatmaps(count: 50, randomDifficulties: false);
|
loadBeatmaps(setCount: 50);
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++)
|
for (int i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
@ -150,7 +151,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestManyPanels()
|
public void TestManyPanels()
|
||||||
{
|
{
|
||||||
loadBeatmaps(count: 5000, randomDifficulties: true);
|
loadBeatmaps(setCount: 5000, randomDifficulties: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -501,6 +502,33 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
waitForSelection(set_count);
|
waitForSelection(set_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAddRemoveDifficultySort()
|
||||||
|
{
|
||||||
|
const int local_set_count = 2;
|
||||||
|
const int local_diff_count = 2;
|
||||||
|
|
||||||
|
loadBeatmaps(setCount: local_set_count, diffCount: local_diff_count);
|
||||||
|
|
||||||
|
AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false));
|
||||||
|
|
||||||
|
checkVisibleItemCount(false, local_set_count * local_diff_count);
|
||||||
|
|
||||||
|
var firstAdded = TestResources.CreateTestBeatmapSetInfo(local_diff_count);
|
||||||
|
|
||||||
|
AddStep("Add new set", () => carousel.UpdateBeatmapSet(firstAdded));
|
||||||
|
|
||||||
|
checkVisibleItemCount(false, (local_set_count + 1) * local_diff_count);
|
||||||
|
|
||||||
|
AddStep("Remove set", () => carousel.RemoveBeatmapSet(firstAdded));
|
||||||
|
|
||||||
|
checkVisibleItemCount(false, (local_set_count) * local_diff_count);
|
||||||
|
|
||||||
|
setSelected(local_set_count, 1);
|
||||||
|
|
||||||
|
waitForSelection(local_set_count);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSelectionEnteringFromEmptyRuleset()
|
public void TestSelectionEnteringFromEmptyRuleset()
|
||||||
{
|
{
|
||||||
@ -662,7 +690,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
for (int i = 0; i < 3; i++)
|
for (int i = 0; i < 3; i++)
|
||||||
{
|
{
|
||||||
var set = TestResources.CreateTestBeatmapSetInfo(3);
|
var set = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||||
|
|
||||||
// only need to set the first as they are a shared reference.
|
// only need to set the first as they are a shared reference.
|
||||||
var beatmap = set.Beatmaps.First();
|
var beatmap = set.Beatmaps.First();
|
||||||
@ -709,7 +737,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
for (int i = 0; i < 3; i++)
|
for (int i = 0; i < 3; i++)
|
||||||
{
|
{
|
||||||
var set = TestResources.CreateTestBeatmapSetInfo(3);
|
var set = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||||
|
|
||||||
// only need to set the first as they are a shared reference.
|
// only need to set the first as they are a shared reference.
|
||||||
var beatmap = set.Beatmaps.First();
|
var beatmap = set.Beatmaps.First();
|
||||||
@ -758,32 +786,54 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSortingWithFiltered()
|
public void TestSortingWithDifficultyFiltered()
|
||||||
{
|
{
|
||||||
|
const int local_diff_count = 3;
|
||||||
|
const int local_set_count = 2;
|
||||||
|
|
||||||
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
|
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
|
||||||
|
|
||||||
AddStep("Populuate beatmap sets", () =>
|
AddStep("Populuate beatmap sets", () =>
|
||||||
{
|
{
|
||||||
sets.Clear();
|
sets.Clear();
|
||||||
|
|
||||||
for (int i = 0; i < 3; i++)
|
for (int i = 0; i < local_set_count; i++)
|
||||||
{
|
{
|
||||||
var set = TestResources.CreateTestBeatmapSetInfo(3);
|
var set = TestResources.CreateTestBeatmapSetInfo(local_diff_count);
|
||||||
set.Beatmaps[0].StarRating = 3 - i;
|
set.Beatmaps[0].StarRating = 3 - i;
|
||||||
set.Beatmaps[2].StarRating = 6 + i;
|
set.Beatmaps[1].StarRating = 6 + i;
|
||||||
sets.Add(set);
|
sets.Add(set);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
loadBeatmaps(sets);
|
loadBeatmaps(sets);
|
||||||
|
|
||||||
|
AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false));
|
||||||
|
|
||||||
|
checkVisibleItemCount(false, local_set_count * local_diff_count);
|
||||||
|
checkVisibleItemCount(true, 1);
|
||||||
|
|
||||||
AddStep("Filter to normal", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Normal" }, false));
|
AddStep("Filter to normal", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Normal" }, false));
|
||||||
AddAssert("Check first set at end", () => carousel.BeatmapSets.First().Equals(sets.Last()));
|
checkVisibleItemCount(false, local_set_count);
|
||||||
AddAssert("Check last set at start", () => carousel.BeatmapSets.Last().Equals(sets.First()));
|
checkVisibleItemCount(true, 1);
|
||||||
|
|
||||||
|
AddUntilStep("Check all visible sets have one normal", () =>
|
||||||
|
{
|
||||||
|
return carousel.Items.OfType<DrawableCarouselBeatmapSet>()
|
||||||
|
.Where(p => p.IsPresent)
|
||||||
|
.Count(p => ((CarouselBeatmapSet)p.Item)!.Beatmaps.Single().BeatmapInfo.DifficultyName.StartsWith("Normal", StringComparison.Ordinal)) == local_set_count;
|
||||||
|
});
|
||||||
|
|
||||||
AddStep("Filter to insane", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Insane" }, false));
|
AddStep("Filter to insane", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Insane" }, false));
|
||||||
AddAssert("Check first set at start", () => carousel.BeatmapSets.First().Equals(sets.First()));
|
checkVisibleItemCount(false, local_set_count);
|
||||||
AddAssert("Check last set at end", () => carousel.BeatmapSets.Last().Equals(sets.Last()));
|
checkVisibleItemCount(true, 1);
|
||||||
|
|
||||||
|
AddUntilStep("Check all visible sets have one insane", () =>
|
||||||
|
{
|
||||||
|
return carousel.Items.OfType<DrawableCarouselBeatmapSet>()
|
||||||
|
.Where(p => p.IsPresent)
|
||||||
|
.Count(p => ((CarouselBeatmapSet)p.Item)!.Beatmaps.Single().BeatmapInfo.DifficultyName.StartsWith("Insane", StringComparison.Ordinal)) == local_set_count;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -838,7 +888,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
AddStep("create hidden set", () =>
|
AddStep("create hidden set", () =>
|
||||||
{
|
{
|
||||||
hidingSet = TestResources.CreateTestBeatmapSetInfo(3);
|
hidingSet = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||||
hidingSet.Beatmaps[1].Hidden = true;
|
hidingSet.Beatmaps[1].Hidden = true;
|
||||||
|
|
||||||
hiddenList.Clear();
|
hiddenList.Clear();
|
||||||
@ -885,7 +935,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
AddStep("add mixed ruleset beatmapset", () =>
|
AddStep("add mixed ruleset beatmapset", () =>
|
||||||
{
|
{
|
||||||
testMixed = TestResources.CreateTestBeatmapSetInfo(3);
|
testMixed = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||||
|
|
||||||
for (int i = 0; i <= 2; i++)
|
for (int i = 0; i <= 2; i++)
|
||||||
{
|
{
|
||||||
@ -907,7 +957,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
BeatmapSetInfo testSingle = null;
|
BeatmapSetInfo testSingle = null;
|
||||||
AddStep("add single ruleset beatmapset", () =>
|
AddStep("add single ruleset beatmapset", () =>
|
||||||
{
|
{
|
||||||
testSingle = TestResources.CreateTestBeatmapSetInfo(3);
|
testSingle = TestResources.CreateTestBeatmapSetInfo(diff_count);
|
||||||
testSingle.Beatmaps.ForEach(b =>
|
testSingle.Beatmaps.ForEach(b =>
|
||||||
{
|
{
|
||||||
b.Ruleset = rulesets.AvailableRulesets.ElementAt(1);
|
b.Ruleset = rulesets.AvailableRulesets.ElementAt(1);
|
||||||
@ -930,7 +980,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
manySets.Clear();
|
manySets.Clear();
|
||||||
|
|
||||||
for (int i = 1; i <= 50; i++)
|
for (int i = 1; i <= 50; i++)
|
||||||
manySets.Add(TestResources.CreateTestBeatmapSetInfo(3));
|
manySets.Add(TestResources.CreateTestBeatmapSetInfo(diff_count));
|
||||||
});
|
});
|
||||||
|
|
||||||
loadBeatmaps(manySets);
|
loadBeatmaps(manySets);
|
||||||
@ -955,6 +1005,43 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1);
|
AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCarouselRemembersSelectionDifficultySort()
|
||||||
|
{
|
||||||
|
List<BeatmapSetInfo> manySets = new List<BeatmapSetInfo>();
|
||||||
|
|
||||||
|
AddStep("Populate beatmap sets", () =>
|
||||||
|
{
|
||||||
|
manySets.Clear();
|
||||||
|
|
||||||
|
for (int i = 1; i <= 50; i++)
|
||||||
|
manySets.Add(TestResources.CreateTestBeatmapSetInfo(diff_count));
|
||||||
|
});
|
||||||
|
|
||||||
|
loadBeatmaps(manySets);
|
||||||
|
|
||||||
|
AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false));
|
||||||
|
|
||||||
|
advanceSelection(direction: 1, diff: false);
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
{
|
||||||
|
AddStep("Toggle non-matching filter", () =>
|
||||||
|
{
|
||||||
|
carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("Restore no filter", () =>
|
||||||
|
{
|
||||||
|
carousel.Filter(new FilterCriteria(), false);
|
||||||
|
eagerSelectedIDs.Add(carousel.SelectedBeatmapSet!.ID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// always returns to same selection as long as it's available.
|
||||||
|
AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestFilteringByUserStarDifficulty()
|
public void TestFilteringByUserStarDifficulty()
|
||||||
{
|
{
|
||||||
@ -1081,8 +1168,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null, int? count = null,
|
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null,
|
||||||
bool randomDifficulties = false)
|
int? setCount = null, int? diffCount = null, bool randomDifficulties = false)
|
||||||
{
|
{
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
|
|
||||||
@ -1090,11 +1177,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
{
|
{
|
||||||
beatmapSets = new List<BeatmapSetInfo>();
|
beatmapSets = new List<BeatmapSetInfo>();
|
||||||
|
|
||||||
for (int i = 1; i <= (count ?? set_count); i++)
|
for (int i = 1; i <= (setCount ?? set_count); i++)
|
||||||
{
|
{
|
||||||
beatmapSets.Add(randomDifficulties
|
beatmapSets.Add(randomDifficulties
|
||||||
? TestResources.CreateTestBeatmapSetInfo()
|
? TestResources.CreateTestBeatmapSetInfo()
|
||||||
: TestResources.CreateTestBeatmapSetInfo(3));
|
: TestResources.CreateTestBeatmapSetInfo(diffCount ?? diff_count));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Resources.Localisation.Web;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Catch;
|
using osu.Game.Rulesets.Catch;
|
||||||
using osu.Game.Rulesets.Mania;
|
using osu.Game.Rulesets.Mania;
|
||||||
@ -188,7 +189,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
{
|
{
|
||||||
AddUntilStep($"displayed bpm is {target}", () =>
|
AddUntilStep($"displayed bpm is {target}", () =>
|
||||||
{
|
{
|
||||||
var label = infoWedge.DisplayedContent.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Single(l => l.Statistic.Name == "BPM");
|
var label = infoWedge.DisplayedContent.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Single(l => l.Statistic.Name == BeatmapsetsStrings.ShowStatsBpm);
|
||||||
return label.Statistic.Content == target;
|
return label.Statistic.Content == target;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ using osu.Game.Overlays;
|
|||||||
using osu.Game.Overlays.Dialog;
|
using osu.Game.Overlays.Dialog;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
using osu.Game.Screens.Select.Carousel;
|
using osu.Game.Screens.Select.Carousel;
|
||||||
|
using osu.Game.Screens.Select.Filter;
|
||||||
using osu.Game.Tests.Online;
|
using osu.Game.Tests.Online;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
@ -192,6 +193,57 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
AddStep("release mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
|
AddStep("release mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSplitDisplay()
|
||||||
|
{
|
||||||
|
ArchiveDownloadRequest<IBeatmapSetInfo>? downloadRequest = null;
|
||||||
|
|
||||||
|
AddStep("set difficulty sort mode", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }));
|
||||||
|
AddStep("update online hash", () =>
|
||||||
|
{
|
||||||
|
testBeatmapSetInfo.Beatmaps.First().OnlineMD5Hash = "different hash";
|
||||||
|
testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
|
||||||
|
|
||||||
|
carousel.UpdateBeatmapSet(testBeatmapSetInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("multiple \"sets\" visible", () => carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().Count(), () => Is.GreaterThan(1));
|
||||||
|
AddUntilStep("update button visible", getUpdateButton, () => Is.Not.Null);
|
||||||
|
|
||||||
|
AddStep("click button", () => getUpdateButton()?.TriggerClick());
|
||||||
|
|
||||||
|
AddUntilStep("wait for download started", () =>
|
||||||
|
{
|
||||||
|
downloadRequest = beatmapDownloader.GetExistingDownload(testBeatmapSetInfo);
|
||||||
|
return downloadRequest != null;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for button disabled", () => getUpdateButton()?.Enabled.Value == false);
|
||||||
|
|
||||||
|
AddUntilStep("progress download to completion", () =>
|
||||||
|
{
|
||||||
|
if (downloadRequest is TestSceneOnlinePlayBeatmapAvailabilityTracker.TestDownloadRequest testRequest)
|
||||||
|
{
|
||||||
|
testRequest.SetProgress(testRequest.Progress + 0.1f);
|
||||||
|
|
||||||
|
if (testRequest.Progress >= 1)
|
||||||
|
{
|
||||||
|
testRequest.TriggerSuccess();
|
||||||
|
|
||||||
|
// usually this would be done by the import process.
|
||||||
|
testBeatmapSetInfo.Beatmaps.First().MD5Hash = "different hash";
|
||||||
|
testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
|
||||||
|
|
||||||
|
// usually this would be done by a realm subscription.
|
||||||
|
carousel.UpdateBeatmapSet(testBeatmapSetInfo);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private BeatmapCarousel createCarousel()
|
private BeatmapCarousel createCarousel()
|
||||||
{
|
{
|
||||||
return carousel = new BeatmapCarousel
|
return carousel = new BeatmapCarousel
|
||||||
@ -199,7 +251,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
BeatmapSets = new List<BeatmapSetInfo>
|
BeatmapSets = new List<BeatmapSetInfo>
|
||||||
{
|
{
|
||||||
(testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo()),
|
(testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(5)),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -58,9 +58,14 @@ namespace osu.Game.Tournament.Tests.Components
|
|||||||
|
|
||||||
songBar.Beatmap = new TournamentBeatmap(beatmap);
|
songBar.Beatmap = new TournamentBeatmap(beatmap);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("set mods to HR", () => songBar.Mods = LegacyMods.HardRock);
|
AddStep("set mods to HR", () => songBar.Mods = LegacyMods.HardRock);
|
||||||
AddStep("set mods to DT", () => songBar.Mods = LegacyMods.DoubleTime);
|
AddStep("set mods to DT", () => songBar.Mods = LegacyMods.DoubleTime);
|
||||||
AddStep("unset mods", () => songBar.Mods = LegacyMods.None);
|
AddStep("unset mods", () => songBar.Mods = LegacyMods.None);
|
||||||
|
|
||||||
|
AddToggleStep("toggle expanded", expanded => songBar.Expanded = expanded);
|
||||||
|
|
||||||
|
AddStep("set null beatmap", () => songBar.Beatmap = null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,13 @@ namespace osu.Game.Tournament.Tests.Screens
|
|||||||
{
|
{
|
||||||
public partial class TestSceneScheduleScreen : TournamentScreenTestScene
|
public partial class TestSceneScheduleScreen : TournamentScreenTestScene
|
||||||
{
|
{
|
||||||
|
public override void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("clear matches", () => Ladder.Matches.Clear());
|
||||||
|
|
||||||
|
base.SetUpSteps();
|
||||||
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
@ -34,6 +41,36 @@ namespace osu.Game.Tournament.Tests.Screens
|
|||||||
AddStep("Set null current match", () => Ladder.CurrentMatch.Value = null);
|
AddStep("Set null current match", () => Ladder.CurrentMatch.Value = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUpcomingMatches()
|
||||||
|
{
|
||||||
|
AddStep("Add upcoming match", () =>
|
||||||
|
{
|
||||||
|
var tournamentMatch = CreateSampleMatch();
|
||||||
|
|
||||||
|
tournamentMatch.Date.Value = DateTimeOffset.UtcNow.AddMinutes(5);
|
||||||
|
tournamentMatch.Completed.Value = false;
|
||||||
|
|
||||||
|
Ladder.Matches.Add(tournamentMatch);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRecentMatches()
|
||||||
|
{
|
||||||
|
AddStep("Add recent match", () =>
|
||||||
|
{
|
||||||
|
var tournamentMatch = CreateSampleMatch();
|
||||||
|
|
||||||
|
tournamentMatch.Date.Value = DateTimeOffset.UtcNow;
|
||||||
|
tournamentMatch.Completed.Value = true;
|
||||||
|
tournamentMatch.Team1Score.Value = tournamentMatch.PointsToWin;
|
||||||
|
tournamentMatch.Team2Score.Value = tournamentMatch.PointsToWin / 2;
|
||||||
|
|
||||||
|
Ladder.Matches.Add(tournamentMatch);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void setMatchDate(TimeSpan relativeTime)
|
private void setMatchDate(TimeSpan relativeTime)
|
||||||
// Humanizer cannot handle negative timespans.
|
// Humanizer cannot handle negative timespans.
|
||||||
=> AddStep($"start time is {relativeTime}", () =>
|
=> AddStep($"start time is {relativeTime}", () =>
|
||||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Tournament.Tests
|
|||||||
{
|
{
|
||||||
public TournamentScalingContainer()
|
public TournamentScalingContainer()
|
||||||
{
|
{
|
||||||
TargetDrawSize = new Vector2(1920, 1080);
|
TargetDrawSize = new Vector2(1024, 768);
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,8 +92,16 @@ namespace osu.Game.Tournament.IPC
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
beatmapLookupRequest = new GetBeatmapRequest(new APIBeatmap { OnlineID = beatmapId });
|
beatmapLookupRequest = new GetBeatmapRequest(new APIBeatmap { OnlineID = beatmapId });
|
||||||
beatmapLookupRequest.Success += b => Beatmap.Value = new TournamentBeatmap(b);
|
beatmapLookupRequest.Success += b =>
|
||||||
beatmapLookupRequest.Failure += _ => Beatmap.Value = null;
|
{
|
||||||
|
if (lastBeatmapId == beatmapId)
|
||||||
|
Beatmap.Value = new TournamentBeatmap(b);
|
||||||
|
};
|
||||||
|
beatmapLookupRequest.Failure += _ =>
|
||||||
|
{
|
||||||
|
if (lastBeatmapId == beatmapId)
|
||||||
|
Beatmap.Value = null;
|
||||||
|
};
|
||||||
API.Queue(beatmapLookupRequest);
|
API.Queue(beatmapLookupRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,16 @@ using osu.Framework.Extensions.Color4Extensions;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Tournament
|
namespace osu.Game.Tournament
|
||||||
{
|
{
|
||||||
internal partial class SaveChangesOverlay : CompositeDrawable
|
internal partial class SaveChangesOverlay : CompositeDrawable, IKeyBindingHandler<PlatformAction>
|
||||||
{
|
{
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private TournamentGame tournamentGame { get; set; } = null!;
|
private TournamentGame tournamentGame { get; set; } = null!;
|
||||||
@ -78,6 +81,21 @@ namespace osu.Game.Tournament
|
|||||||
scheduleNextCheck();
|
scheduleNextCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
|
||||||
|
{
|
||||||
|
if (e.Action == PlatformAction.Save && !e.Repeat)
|
||||||
|
{
|
||||||
|
saveChangesButton.TriggerClick();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
private void scheduleNextCheck() => Scheduler.AddDelayed(() => checkForChanges().FireAndForget(), 1000);
|
private void scheduleNextCheck() => Scheduler.AddDelayed(() => checkForChanges().FireAndForget(), 1000);
|
||||||
|
|
||||||
private void saveChanges()
|
private void saveChanges()
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// 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.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -19,6 +20,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
{
|
{
|
||||||
public partial class ScheduleScreen : TournamentScreen
|
public partial class ScheduleScreen : TournamentScreen
|
||||||
{
|
{
|
||||||
|
private readonly BindableList<TournamentMatch> allMatches = new BindableList<TournamentMatch>();
|
||||||
private readonly Bindable<TournamentMatch?> currentMatch = new Bindable<TournamentMatch?>();
|
private readonly Bindable<TournamentMatch?> currentMatch = new Bindable<TournamentMatch?>();
|
||||||
private Container mainContainer = null!;
|
private Container mainContainer = null!;
|
||||||
private LadderInfo ladder = null!;
|
private LadderInfo ladder = null!;
|
||||||
@ -101,19 +103,34 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
|
allMatches.BindTo(ladder.Matches);
|
||||||
|
allMatches.BindCollectionChanged((_, _) => refresh());
|
||||||
|
|
||||||
currentMatch.BindTo(ladder.CurrentMatch);
|
currentMatch.BindTo(ladder.CurrentMatch);
|
||||||
currentMatch.BindValueChanged(matchChanged, true);
|
currentMatch.BindValueChanged(_ => refresh(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void matchChanged(ValueChangedEvent<TournamentMatch?> match)
|
private void refresh()
|
||||||
{
|
{
|
||||||
var upcoming = ladder.Matches.Where(p => !p.Completed.Value && p.Team1.Value != null && p.Team2.Value != null && Math.Abs(p.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < 4);
|
const int days_for_displays = 4;
|
||||||
var conditionals = ladder
|
|
||||||
.Matches.Where(p => !p.Completed.Value && (p.Team1.Value == null || p.Team2.Value == null) && Math.Abs(p.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < 4)
|
|
||||||
.SelectMany(m => m.ConditionalMatches.Where(cp => m.Acronyms.TrueForAll(a => cp.Acronyms.Contains(a))));
|
|
||||||
|
|
||||||
upcoming = upcoming.Concat(conditionals);
|
IEnumerable<ConditionalTournamentMatch> conditionals =
|
||||||
upcoming = upcoming.OrderBy(p => p.Date.Value).Take(8);
|
allMatches
|
||||||
|
.Where(m => !m.Completed.Value && (m.Team1.Value == null || m.Team2.Value == null) && Math.Abs(m.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < days_for_displays)
|
||||||
|
.SelectMany(m => m.ConditionalMatches.Where(cp => m.Acronyms.TrueForAll(a => cp.Acronyms.Contains(a))));
|
||||||
|
|
||||||
|
IEnumerable<TournamentMatch> upcoming =
|
||||||
|
allMatches
|
||||||
|
.Where(m => !m.Completed.Value && m.Team1.Value != null && m.Team2.Value != null && Math.Abs(m.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < days_for_displays)
|
||||||
|
.Concat(conditionals)
|
||||||
|
.OrderBy(m => m.Date.Value)
|
||||||
|
.Take(8);
|
||||||
|
|
||||||
|
var recent =
|
||||||
|
allMatches
|
||||||
|
.Where(m => m.Completed.Value && m.Team1.Value != null && m.Team2.Value != null && Math.Abs(m.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < days_for_displays)
|
||||||
|
.OrderByDescending(m => m.Date.Value)
|
||||||
|
.Take(8);
|
||||||
|
|
||||||
ScheduleContainer comingUpNext;
|
ScheduleContainer comingUpNext;
|
||||||
|
|
||||||
@ -137,12 +154,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Width = 0.4f,
|
Width = 0.4f,
|
||||||
ChildrenEnumerable = ladder.Matches
|
ChildrenEnumerable = recent.Select(p => new ScheduleMatch(p))
|
||||||
.Where(p => p.Completed.Value && p.Team1.Value != null && p.Team2.Value != null
|
|
||||||
&& Math.Abs(p.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < 4)
|
|
||||||
.OrderByDescending(p => p.Date.Value)
|
|
||||||
.Take(8)
|
|
||||||
.Select(p => new ScheduleMatch(p))
|
|
||||||
},
|
},
|
||||||
new ScheduleContainer("upcoming matches")
|
new ScheduleContainer("upcoming matches")
|
||||||
{
|
{
|
||||||
@ -161,7 +173,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (match.NewValue != null)
|
if (currentMatch.Value != null)
|
||||||
{
|
{
|
||||||
comingUpNext.Child = new FillFlowContainer
|
comingUpNext.Child = new FillFlowContainer
|
||||||
{
|
{
|
||||||
@ -170,12 +182,12 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
Spacing = new Vector2(30),
|
Spacing = new Vector2(30),
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new ScheduleMatch(match.NewValue, false)
|
new ScheduleMatch(currentMatch.Value, false)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
},
|
},
|
||||||
new TournamentSpriteTextWithBackground(match.NewValue.Round.Value?.Name.Value ?? string.Empty)
|
new TournamentSpriteTextWithBackground(currentMatch.Value.Round.Value?.Name.Value ?? string.Empty)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
@ -185,7 +197,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Text = match.NewValue.Team1.Value?.FullName + " vs " + match.NewValue.Team2.Value?.FullName,
|
Text = currentMatch.Value.Team1.Value?.FullName + " vs " + currentMatch.Value.Team2.Value?.FullName,
|
||||||
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold)
|
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold)
|
||||||
},
|
},
|
||||||
new FillFlowContainer
|
new FillFlowContainer
|
||||||
@ -196,7 +208,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new ScheduleMatchDate(match.NewValue.Date.Value)
|
new ScheduleMatchDate(currentMatch.Value.Date.Value)
|
||||||
{
|
{
|
||||||
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular)
|
Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular)
|
||||||
}
|
}
|
||||||
@ -282,6 +294,7 @@ namespace osu.Game.Tournament.Screens.Schedule
|
|||||||
{
|
{
|
||||||
Direction = FillDirection.Vertical,
|
Direction = FillDirection.Vertical,
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Spacing = new Vector2(0, -6),
|
||||||
Margin = new MarginPadding(10)
|
Margin = new MarginPadding(10)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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.Drawing;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -48,8 +47,6 @@ namespace osu.Game.Tournament
|
|||||||
{
|
{
|
||||||
frameworkConfig.BindWith(FrameworkSetting.WindowedSize, windowSize);
|
frameworkConfig.BindWith(FrameworkSetting.WindowedSize, windowSize);
|
||||||
|
|
||||||
windowSize.MinValue = new Size(TournamentSceneManager.REQUIRED_WIDTH, TournamentSceneManager.STREAM_AREA_HEIGHT);
|
|
||||||
|
|
||||||
windowMode = frameworkConfig.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
|
windowMode = frameworkConfig.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
|
||||||
|
|
||||||
Add(loadingSpinner = new LoadingSpinner(true, true)
|
Add(loadingSpinner = new LoadingSpinner(true, true)
|
||||||
|
@ -23,6 +23,9 @@ namespace osu.Game.Collections
|
|||||||
|
|
||||||
private AudioFilter lowPassFilter = null!;
|
private AudioFilter lowPassFilter = null!;
|
||||||
|
|
||||||
|
protected override string PopInSampleName => @"UI/overlay-big-pop-in";
|
||||||
|
protected override string PopOutSampleName => @"UI/overlay-big-pop-out";
|
||||||
|
|
||||||
public ManageCollectionsDialog()
|
public ManageCollectionsDialog()
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre;
|
Anchor = Anchor.Centre;
|
||||||
|
@ -15,6 +15,7 @@ using osu.Framework.Localisation;
|
|||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Game.Online;
|
using osu.Game.Online;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
using osu.Game.Localisation;
|
||||||
|
|
||||||
namespace osu.Game.Graphics.Containers
|
namespace osu.Game.Graphics.Containers
|
||||||
{
|
{
|
||||||
@ -74,7 +75,7 @@ namespace osu.Game.Graphics.Containers
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void AddUserLink(IUser user, Action<SpriteText> creationParameters = null)
|
public void AddUserLink(IUser user, Action<SpriteText> creationParameters = null)
|
||||||
=> createLink(CreateChunkFor(user.Username, true, CreateSpriteText, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user), "view profile");
|
=> createLink(CreateChunkFor(user.Username, true, CreateSpriteText, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user), ContextMenuStrings.ViewProfile);
|
||||||
|
|
||||||
private void createLink(ITextPart textPart, LinkDetails link, LocalisableString tooltipText, Action action = null)
|
private void createLink(ITextPart textPart, LinkDetails link, LocalisableString tooltipText, Action action = null)
|
||||||
{
|
{
|
||||||
|
@ -24,6 +24,7 @@ namespace osu.Game.Graphics.Containers
|
|||||||
private Sample samplePopOut;
|
private Sample samplePopOut;
|
||||||
protected virtual string PopInSampleName => "UI/overlay-pop-in";
|
protected virtual string PopInSampleName => "UI/overlay-pop-in";
|
||||||
protected virtual string PopOutSampleName => "UI/overlay-pop-out";
|
protected virtual string PopOutSampleName => "UI/overlay-pop-out";
|
||||||
|
protected virtual double PopInOutSampleBalance => 0;
|
||||||
|
|
||||||
protected override bool BlockNonPositionalInput => true;
|
protected override bool BlockNonPositionalInput => true;
|
||||||
|
|
||||||
@ -133,15 +134,21 @@ namespace osu.Game.Graphics.Containers
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (didChange)
|
if (didChange && samplePopIn != null)
|
||||||
samplePopIn?.Play();
|
{
|
||||||
|
samplePopIn.Balance.Value = PopInOutSampleBalance;
|
||||||
|
samplePopIn.Play();
|
||||||
|
}
|
||||||
|
|
||||||
if (BlockScreenWideMouse && DimMainContent) overlayManager?.ShowBlockingOverlay(this);
|
if (BlockScreenWideMouse && DimMainContent) overlayManager?.ShowBlockingOverlay(this);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Visibility.Hidden:
|
case Visibility.Hidden:
|
||||||
if (didChange)
|
if (didChange && samplePopOut != null)
|
||||||
samplePopOut?.Play();
|
{
|
||||||
|
samplePopOut.Balance.Value = PopInOutSampleBalance;
|
||||||
|
samplePopOut.Play();
|
||||||
|
}
|
||||||
|
|
||||||
if (BlockScreenWideMouse) overlayManager?.HideBlockingOverlay(this);
|
if (BlockScreenWideMouse) overlayManager?.HideBlockingOverlay(this);
|
||||||
break;
|
break;
|
||||||
|
@ -2,6 +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 osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Audio.Sample;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -32,6 +35,12 @@ namespace osu.Game.Graphics.Containers
|
|||||||
|
|
||||||
protected override bool StartHidden => true;
|
protected override bool StartHidden => true;
|
||||||
|
|
||||||
|
private Sample? samplePopIn;
|
||||||
|
private Sample? samplePopOut;
|
||||||
|
|
||||||
|
// required due to LoadAsyncComplete() in `VisibilityContainer` calling PopOut() during load - similar workaround to `OsuDropdownMenu`
|
||||||
|
private bool wasShown;
|
||||||
|
|
||||||
public Color4 FirstWaveColour
|
public Color4 FirstWaveColour
|
||||||
{
|
{
|
||||||
get => firstWave.Colour;
|
get => firstWave.Colour;
|
||||||
@ -56,6 +65,13 @@ namespace osu.Game.Graphics.Containers
|
|||||||
set => fourthWave.Colour = value;
|
set => fourthWave.Colour = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader(true)]
|
||||||
|
private void load(AudioManager audio)
|
||||||
|
{
|
||||||
|
samplePopIn = audio.Samples.Get("UI/wave-pop-in");
|
||||||
|
samplePopOut = audio.Samples.Get("UI/overlay-big-pop-out");
|
||||||
|
}
|
||||||
|
|
||||||
public WaveContainer()
|
public WaveContainer()
|
||||||
{
|
{
|
||||||
Masking = true;
|
Masking = true;
|
||||||
@ -110,6 +126,8 @@ namespace osu.Game.Graphics.Containers
|
|||||||
w.Show();
|
w.Show();
|
||||||
|
|
||||||
contentContainer.MoveToY(0, APPEAR_DURATION, Easing.OutQuint);
|
contentContainer.MoveToY(0, APPEAR_DURATION, Easing.OutQuint);
|
||||||
|
samplePopIn?.Play();
|
||||||
|
wasShown = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void PopOut()
|
protected override void PopOut()
|
||||||
@ -118,6 +136,9 @@ namespace osu.Game.Graphics.Containers
|
|||||||
w.Hide();
|
w.Hide();
|
||||||
|
|
||||||
contentContainer.MoveToY(2, DISAPPEAR_DURATION, Easing.In);
|
contentContainer.MoveToY(2, DISAPPEAR_DURATION, Easing.In);
|
||||||
|
|
||||||
|
if (wasShown)
|
||||||
|
samplePopOut?.Play();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateAfterChildren()
|
protected override void UpdateAfterChildren()
|
||||||
|
@ -46,8 +46,8 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
private readonly Container content;
|
private readonly Container content;
|
||||||
private readonly Box hover;
|
private readonly Box hover;
|
||||||
|
|
||||||
public OsuAnimatedButton()
|
public OsuAnimatedButton(HoverSampleSet sampleSet = HoverSampleSet.Button)
|
||||||
: base(HoverSampleSet.Button)
|
: base(sampleSet)
|
||||||
{
|
{
|
||||||
base.Content.Add(content = new Container
|
base.Content.Add(content = new Container
|
||||||
{
|
{
|
||||||
|
@ -14,6 +14,12 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
private Sample? sampleOff;
|
private Sample? sampleOff;
|
||||||
private Sample? sampleOn;
|
private Sample? sampleOn;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sheared toggle buttons by default play two samples when toggled: a click and a toggle (on/off).
|
||||||
|
/// Sometimes this might be too much. Setting this to <c>false</c> will silence the toggle sound.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual bool PlayToggleSamples => true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this button is currently toggled to an active state.
|
/// Whether this button is currently toggled to an active state.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -68,10 +74,13 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
{
|
{
|
||||||
sampleClick?.Play();
|
sampleClick?.Play();
|
||||||
|
|
||||||
if (Active.Value)
|
if (PlayToggleSamples)
|
||||||
sampleOn?.Play();
|
{
|
||||||
else
|
if (Active.Value)
|
||||||
sampleOff?.Play();
|
sampleOn?.Play();
|
||||||
|
else
|
||||||
|
sampleOff?.Play();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
// 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.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Audio.Sample;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -21,6 +23,14 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
|||||||
private const float fade_duration = 250;
|
private const float fade_duration = 250;
|
||||||
private const double scale_duration = 500;
|
private const double scale_duration = 500;
|
||||||
|
|
||||||
|
private Sample? samplePopIn;
|
||||||
|
private Sample? samplePopOut;
|
||||||
|
protected virtual string PopInSampleName => "UI/overlay-pop-in";
|
||||||
|
protected virtual string PopOutSampleName => "UI/overlay-pop-out";
|
||||||
|
|
||||||
|
// required due to LoadAsyncComplete() in `VisibilityContainer` calling PopOut() during load - similar workaround to `OsuDropdownMenu`
|
||||||
|
private bool wasOpened;
|
||||||
|
|
||||||
public OsuPopover(bool withPadding = true)
|
public OsuPopover(bool withPadding = true)
|
||||||
{
|
{
|
||||||
Content.Padding = withPadding ? new MarginPadding(20) : new MarginPadding();
|
Content.Padding = withPadding ? new MarginPadding(20) : new MarginPadding();
|
||||||
@ -38,9 +48,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(OverlayColourProvider? colourProvider, OsuColour colours)
|
private void load(OverlayColourProvider? colourProvider, OsuColour colours, AudioManager audio)
|
||||||
{
|
{
|
||||||
Background.Colour = Arrow.Colour = colourProvider?.Background4 ?? colours.GreySeaFoamDarker;
|
Background.Colour = Arrow.Colour = colourProvider?.Background4 ?? colours.GreySeaFoamDarker;
|
||||||
|
samplePopIn = audio.Samples.Get(PopInSampleName);
|
||||||
|
samplePopOut = audio.Samples.Get(PopOutSampleName);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Drawable CreateArrow() => Empty();
|
protected override Drawable CreateArrow() => Empty();
|
||||||
@ -49,12 +61,18 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
|||||||
{
|
{
|
||||||
this.ScaleTo(1, scale_duration, Easing.OutElasticHalf);
|
this.ScaleTo(1, scale_duration, Easing.OutElasticHalf);
|
||||||
this.FadeIn(fade_duration, Easing.OutQuint);
|
this.FadeIn(fade_duration, Easing.OutQuint);
|
||||||
|
|
||||||
|
samplePopIn?.Play();
|
||||||
|
wasOpened = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void PopOut()
|
protected override void PopOut()
|
||||||
{
|
{
|
||||||
this.ScaleTo(0.7f, scale_duration, Easing.OutQuint);
|
this.ScaleTo(0.7f, scale_duration, Easing.OutQuint);
|
||||||
this.FadeOut(fade_duration, Easing.OutQuint);
|
this.FadeOut(fade_duration, Easing.OutQuint);
|
||||||
|
|
||||||
|
if (wasOpened)
|
||||||
|
samplePopOut?.Play();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnKeyDown(KeyDownEvent e)
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
|
@ -14,6 +14,6 @@ namespace osu.Game.Online.API.Requests
|
|||||||
|
|
||||||
protected override string FileExtension => ".osr";
|
protected override string FileExtension => ".osr";
|
||||||
|
|
||||||
protected override string Target => $@"scores/{Model.Ruleset.ShortName}/{Model.OnlineID}/download";
|
protected override string Target => $@"scores/{Model.OnlineID}/download";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,9 @@ namespace osu.Game.Overlays
|
|||||||
private const float side_bar_width = 190;
|
private const float side_bar_width = 190;
|
||||||
private const float chat_bar_height = 60;
|
private const float chat_bar_height = 60;
|
||||||
|
|
||||||
|
protected override string PopInSampleName => @"UI/overlay-big-pop-in";
|
||||||
|
protected override string PopOutSampleName => @"UI/overlay-big-pop-out";
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuConfigManager config { get; set; } = null!;
|
private OsuConfigManager config { get; set; } = null!;
|
||||||
|
|
||||||
|
@ -20,6 +20,8 @@ namespace osu.Game.Overlays
|
|||||||
|
|
||||||
private const float transition_time = 400;
|
private const float transition_time = 400;
|
||||||
|
|
||||||
|
protected override double PopInOutSampleBalance => OsuGameBase.SFX_STEREO_STRENGTH;
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
|
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||||
|
|
||||||
|
@ -18,6 +18,8 @@ namespace osu.Game.Overlays.Mods
|
|||||||
{
|
{
|
||||||
public partial class AddPresetButton : ShearedToggleButton, IHasPopover
|
public partial class AddPresetButton : ShearedToggleButton, IHasPopover
|
||||||
{
|
{
|
||||||
|
protected override bool PlayToggleSamples => false;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuColour colours { get; set; } = null!;
|
private OsuColour colours { get; set; } = null!;
|
||||||
|
|
||||||
|
@ -31,6 +31,8 @@ namespace osu.Game.Overlays
|
|||||||
public LocalisableString Title => NotificationsStrings.HeaderTitle;
|
public LocalisableString Title => NotificationsStrings.HeaderTitle;
|
||||||
public LocalisableString Description => NotificationsStrings.HeaderDescription;
|
public LocalisableString Description => NotificationsStrings.HeaderDescription;
|
||||||
|
|
||||||
|
protected override double PopInOutSampleBalance => OsuGameBase.SFX_STEREO_STRENGTH;
|
||||||
|
|
||||||
public const float WIDTH = 320;
|
public const float WIDTH = 320;
|
||||||
|
|
||||||
public const float TRANSITION_LENGTH = 600;
|
public const float TRANSITION_LENGTH = 600;
|
||||||
|
@ -56,6 +56,7 @@ namespace osu.Game.Overlays
|
|||||||
private SeekLimitedSearchTextBox searchTextBox;
|
private SeekLimitedSearchTextBox searchTextBox;
|
||||||
|
|
||||||
protected override string PopInSampleName => "UI/settings-pop-in";
|
protected override string PopInSampleName => "UI/settings-pop-in";
|
||||||
|
protected override double PopInOutSampleBalance => -OsuGameBase.SFX_STEREO_STRENGTH;
|
||||||
|
|
||||||
private readonly bool showSidebar;
|
private readonly bool showSidebar;
|
||||||
|
|
||||||
|
@ -47,10 +47,7 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
|
|
||||||
// copy to mutate, as we will need to compare to the original later on.
|
// copy to mutate, as we will need to compare to the original later on.
|
||||||
var adjustedRect = selectionRect;
|
var adjustedRect = selectionRect;
|
||||||
|
bool isRotated = false;
|
||||||
// first, remove any scale axis we are not interested in.
|
|
||||||
if (anchor.HasFlagFast(Anchor.x1)) scale.X = 0;
|
|
||||||
if (anchor.HasFlagFast(Anchor.y1)) scale.Y = 0;
|
|
||||||
|
|
||||||
// for now aspect lock scale adjustments that occur at corners..
|
// for now aspect lock scale adjustments that occur at corners..
|
||||||
if (!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1))
|
if (!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1))
|
||||||
@ -61,8 +58,9 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
}
|
}
|
||||||
// ..or if any of the selection have been rotated.
|
// ..or if any of the selection have been rotated.
|
||||||
// this is to avoid requiring skew logic (which would likely not be the user's expected transform anyway).
|
// this is to avoid requiring skew logic (which would likely not be the user's expected transform anyway).
|
||||||
else if (SelectedBlueprints.Any(b => !Precision.AlmostEquals(((Drawable)b.Item).Rotation, 0)))
|
else if (SelectedBlueprints.Any(b => !Precision.AlmostEquals(((Drawable)b.Item).Rotation % 90, 0)))
|
||||||
{
|
{
|
||||||
|
isRotated = true;
|
||||||
if (anchor.HasFlagFast(Anchor.x1))
|
if (anchor.HasFlagFast(Anchor.x1))
|
||||||
// if dragging from the horizontal centre, only a vertical component is available.
|
// if dragging from the horizontal centre, only a vertical component is available.
|
||||||
scale.X = scale.Y / selectionRect.Height * selectionRect.Width;
|
scale.X = scale.Y / selectionRect.Height * selectionRect.Width;
|
||||||
@ -74,13 +72,28 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
if (anchor.HasFlagFast(Anchor.x0)) adjustedRect.X -= scale.X;
|
if (anchor.HasFlagFast(Anchor.x0)) adjustedRect.X -= scale.X;
|
||||||
if (anchor.HasFlagFast(Anchor.y0)) adjustedRect.Y -= scale.Y;
|
if (anchor.HasFlagFast(Anchor.y0)) adjustedRect.Y -= scale.Y;
|
||||||
|
|
||||||
|
// Maintain the selection's centre position if dragging from the centre anchors and selection is rotated.
|
||||||
|
if (isRotated && anchor.HasFlagFast(Anchor.x1)) adjustedRect.X -= scale.X / 2;
|
||||||
|
if (isRotated && anchor.HasFlagFast(Anchor.y1)) adjustedRect.Y -= scale.Y / 2;
|
||||||
|
|
||||||
adjustedRect.Width += scale.X;
|
adjustedRect.Width += scale.X;
|
||||||
adjustedRect.Height += scale.Y;
|
adjustedRect.Height += scale.Y;
|
||||||
|
|
||||||
|
if (adjustedRect.Width <= 0 || adjustedRect.Height <= 0)
|
||||||
|
{
|
||||||
|
Axes toFlip = Axes.None;
|
||||||
|
|
||||||
|
if (adjustedRect.Width <= 0) toFlip |= Axes.X;
|
||||||
|
if (adjustedRect.Height <= 0) toFlip |= Axes.Y;
|
||||||
|
|
||||||
|
SelectionBox.PerformFlipFromScaleHandles(toFlip);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// scale adjust applied to each individual item should match that of the quad itself.
|
// scale adjust applied to each individual item should match that of the quad itself.
|
||||||
var scaledDelta = new Vector2(
|
var scaledDelta = new Vector2(
|
||||||
MathF.Max(adjustedRect.Width / selectionRect.Width, 0),
|
adjustedRect.Width / selectionRect.Width,
|
||||||
MathF.Max(adjustedRect.Height / selectionRect.Height, 0)
|
adjustedRect.Height / selectionRect.Height
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach (var b in SelectedBlueprints)
|
foreach (var b in SelectedBlueprints)
|
||||||
@ -102,7 +115,12 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
);
|
);
|
||||||
|
|
||||||
updateDrawablePosition(drawableItem, newPositionInAdjusted);
|
updateDrawablePosition(drawableItem, newPositionInAdjusted);
|
||||||
drawableItem.Scale *= scaledDelta;
|
|
||||||
|
var currentScaledDelta = scaledDelta;
|
||||||
|
if (Precision.AlmostEquals(MathF.Abs(drawableItem.Rotation) % 180, 90))
|
||||||
|
currentScaledDelta = new Vector2(scaledDelta.Y, scaledDelta.X);
|
||||||
|
|
||||||
|
drawableItem.Scale *= currentScaledDelta;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -18,7 +18,9 @@ namespace osu.Game.Overlays
|
|||||||
|
|
||||||
protected override bool StartHidden => true;
|
protected override bool StartHidden => true;
|
||||||
|
|
||||||
protected override string PopInSampleName => "UI/wave-pop-in";
|
// `WaveContainer` plays PopIn/PopOut samples, so we disable the overlay-level one as to not double-up sample playback.
|
||||||
|
protected override string PopInSampleName => string.Empty;
|
||||||
|
protected override string PopOutSampleName => string.Empty;
|
||||||
|
|
||||||
public const float HORIZONTAL_PADDING = 50;
|
public const float HORIZONTAL_PADDING = 50;
|
||||||
|
|
||||||
|
@ -71,8 +71,21 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
{
|
{
|
||||||
var bindable = (IBindable)property.GetValue(this)!;
|
var bindable = (IBindable)property.GetValue(this)!;
|
||||||
|
|
||||||
|
string valueText;
|
||||||
|
|
||||||
|
switch (bindable)
|
||||||
|
{
|
||||||
|
case Bindable<bool> b:
|
||||||
|
valueText = b.Value ? "on" : "off";
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
valueText = bindable.ToString() ?? string.Empty;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (!bindable.IsDefault)
|
if (!bindable.IsDefault)
|
||||||
tooltipTexts.Add($"{attr.Label} {bindable}");
|
tooltipTexts.Add($"{attr.Label}: {valueText}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s)));
|
return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s)));
|
||||||
|
@ -21,8 +21,9 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
{
|
{
|
||||||
User = new APIUser
|
User = new APIUser
|
||||||
{
|
{
|
||||||
Id = APIUser.SYSTEM_USER_ID,
|
Id = replayData.User.OnlineID,
|
||||||
Username = replayData.User.Username,
|
Username = replayData.User.Username,
|
||||||
|
IsBot = replayData.User.IsBot,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -16,5 +16,6 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
public override ModType Type => ModType.System;
|
public override ModType Type => ModType.System;
|
||||||
public override LocalisableString Description => "Score set on earlier osu! versions with the V2 scoring algorithm active.";
|
public override LocalisableString Description => "Score set on earlier osu! versions with the V2 scoring algorithm active.";
|
||||||
public override double ScoreMultiplier => 1;
|
public override double ScoreMultiplier => 1;
|
||||||
|
public override bool UserPlayable => false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -262,6 +262,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
private readonly OsuSpriteText divisorText;
|
private readonly OsuSpriteText divisorText;
|
||||||
|
|
||||||
public DivisorDisplay()
|
public DivisorDisplay()
|
||||||
|
: base(HoverSampleSet.Default)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre;
|
Anchor = Anchor.Centre;
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.EnumExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
@ -307,6 +308,25 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// This method should be called when a selection needs to be flipped
|
||||||
|
/// because of an ongoing scale handle drag that would otherwise cause width or height to go negative.
|
||||||
|
/// </remarks>
|
||||||
|
public void PerformFlipFromScaleHandles(Axes axes)
|
||||||
|
{
|
||||||
|
if (axes.HasFlagFast(Axes.X))
|
||||||
|
{
|
||||||
|
dragHandles.FlipScaleHandles(Direction.Horizontal);
|
||||||
|
OnFlip?.Invoke(Direction.Horizontal, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (axes.HasFlagFast(Axes.Y))
|
||||||
|
{
|
||||||
|
dragHandles.FlipScaleHandles(Direction.Vertical);
|
||||||
|
OnFlip?.Invoke(Direction.Vertical, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void addScaleHandle(Anchor anchor)
|
private void addScaleHandle(Anchor anchor)
|
||||||
{
|
{
|
||||||
var handle = new SelectionBoxScaleHandle
|
var handle = new SelectionBoxScaleHandle
|
||||||
|
@ -7,6 +7,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions.EnumExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
|
||||||
@ -69,6 +70,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
allDragHandles.Add(handle);
|
allDragHandles.Add(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void FlipScaleHandles(Direction direction)
|
||||||
|
{
|
||||||
|
foreach (var handle in scaleHandles)
|
||||||
|
{
|
||||||
|
if (direction == Direction.Horizontal && !handle.Anchor.HasFlagFast(Anchor.x1))
|
||||||
|
handle.Anchor ^= Anchor.x0 | Anchor.x2;
|
||||||
|
if (direction == Direction.Vertical && !handle.Anchor.HasFlagFast(Anchor.y1))
|
||||||
|
handle.Anchor ^= Anchor.y0 | Anchor.y2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private SelectionBoxRotationHandle displayedRotationHandle;
|
private SelectionBoxRotationHandle displayedRotationHandle;
|
||||||
private SelectionBoxDragHandle activeHandle;
|
private SelectionBoxDragHandle activeHandle;
|
||||||
|
|
||||||
|
@ -114,6 +114,9 @@ namespace osu.Game.Screens.Edit.Setup
|
|||||||
|
|
||||||
private partial class FileChooserPopover : OsuPopover
|
private partial class FileChooserPopover : OsuPopover
|
||||||
{
|
{
|
||||||
|
protected override string PopInSampleName => "UI/overlay-big-pop-in";
|
||||||
|
protected override string PopOutSampleName => "UI/overlay-big-pop-out";
|
||||||
|
|
||||||
public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> currentFile, string? chooserPath)
|
public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> currentFile, string? chooserPath)
|
||||||
{
|
{
|
||||||
Child = new Container
|
Child = new Container
|
||||||
|
@ -170,7 +170,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
|
|||||||
|
|
||||||
if (Room.HasPassword.Value)
|
if (Room.HasPassword.Value)
|
||||||
{
|
{
|
||||||
sampleJoin?.Play();
|
|
||||||
this.ShowPopover();
|
this.ShowPopover();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -156,7 +156,7 @@ namespace osu.Game.Screens.Ranking
|
|||||||
if (Score != null)
|
if (Score != null)
|
||||||
{
|
{
|
||||||
// only show flair / animation when arriving after watching a play that isn't autoplay.
|
// only show flair / animation when arriving after watching a play that isn't autoplay.
|
||||||
bool shouldFlair = player != null && Score.Mods.All(m => m.UserPlayable);
|
bool shouldFlair = player != null && !Score.User.IsBot;
|
||||||
|
|
||||||
ScorePanelList.AddScore(Score, shouldFlair);
|
ScorePanelList.AddScore(Score, shouldFlair);
|
||||||
}
|
}
|
||||||
|
@ -78,6 +78,8 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
private CarouselBeatmapSet? selectedBeatmapSet;
|
private CarouselBeatmapSet? selectedBeatmapSet;
|
||||||
|
|
||||||
|
private List<BeatmapSetInfo> originalBeatmapSetsDetached = new List<BeatmapSetInfo>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Raised when the <see cref="SelectedBeatmapInfo"/> is changed.
|
/// Raised when the <see cref="SelectedBeatmapInfo"/> is changed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -127,15 +129,37 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
private void loadBeatmapSets(IEnumerable<BeatmapSetInfo> beatmapSets)
|
private void loadBeatmapSets(IEnumerable<BeatmapSetInfo> beatmapSets)
|
||||||
{
|
{
|
||||||
|
originalBeatmapSetsDetached = beatmapSets.Detach();
|
||||||
|
|
||||||
|
if (selectedBeatmapSet != null && !originalBeatmapSetsDetached.Contains(selectedBeatmapSet.BeatmapSet))
|
||||||
|
selectedBeatmapSet = null;
|
||||||
|
|
||||||
|
var selectedBeatmapBefore = selectedBeatmap?.BeatmapInfo;
|
||||||
|
|
||||||
CarouselRoot newRoot = new CarouselRoot(this);
|
CarouselRoot newRoot = new CarouselRoot(this);
|
||||||
|
|
||||||
newRoot.AddItems(beatmapSets.Select(s => createCarouselSet(s.Detach())).OfType<CarouselBeatmapSet>());
|
if (beatmapsSplitOut)
|
||||||
|
{
|
||||||
|
var carouselBeatmapSets = originalBeatmapSetsDetached.SelectMany(s => s.Beatmaps).Select(b =>
|
||||||
|
{
|
||||||
|
return createCarouselSet(new BeatmapSetInfo(new[] { b })
|
||||||
|
{
|
||||||
|
ID = b.BeatmapSet!.ID,
|
||||||
|
OnlineID = b.BeatmapSet!.OnlineID
|
||||||
|
});
|
||||||
|
}).OfType<CarouselBeatmapSet>();
|
||||||
|
|
||||||
|
newRoot.AddItems(carouselBeatmapSets);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var carouselBeatmapSets = originalBeatmapSetsDetached.Select(createCarouselSet).OfType<CarouselBeatmapSet>();
|
||||||
|
|
||||||
|
newRoot.AddItems(carouselBeatmapSets);
|
||||||
|
}
|
||||||
|
|
||||||
root = newRoot;
|
root = newRoot;
|
||||||
|
|
||||||
if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet))
|
|
||||||
selectedBeatmapSet = null;
|
|
||||||
|
|
||||||
Scroll.Clear(false);
|
Scroll.Clear(false);
|
||||||
itemsCache.Invalidate();
|
itemsCache.Invalidate();
|
||||||
ScrollToSelected();
|
ScrollToSelected();
|
||||||
@ -144,6 +168,15 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
if (loadedTestBeatmaps)
|
if (loadedTestBeatmaps)
|
||||||
signalBeatmapsLoaded();
|
signalBeatmapsLoaded();
|
||||||
|
|
||||||
|
// Restore selection
|
||||||
|
if (selectedBeatmapBefore != null && newRoot.BeatmapSetsByID.TryGetValue(selectedBeatmapBefore.BeatmapSet!.ID, out var newSelectionCandidates))
|
||||||
|
{
|
||||||
|
CarouselBeatmap? found = newSelectionCandidates.SelectMany(s => s.Beatmaps).SingleOrDefault(b => b.BeatmapInfo.ID == selectedBeatmapBefore.ID);
|
||||||
|
|
||||||
|
if (found != null)
|
||||||
|
found.State.Value = CarouselItemState.Selected;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly List<CarouselItem> visibleItems = new List<CarouselItem>();
|
private readonly List<CarouselItem> visibleItems = new List<CarouselItem>();
|
||||||
@ -330,8 +363,8 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
// Only require to action here if the beatmap is missing.
|
// Only require to action here if the beatmap is missing.
|
||||||
// This avoids processing these events unnecessarily when new beatmaps are imported, for example.
|
// This avoids processing these events unnecessarily when new beatmaps are imported, for example.
|
||||||
if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSet)
|
if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSets)
|
||||||
&& existingSet.BeatmapSet.Beatmaps.All(b => b.ID != beatmapInfo.ID))
|
&& existingSets.SelectMany(s => s.Beatmaps).All(b => b.BeatmapInfo.ID != beatmapInfo.ID))
|
||||||
{
|
{
|
||||||
UpdateBeatmapSet(beatmapSet.Detach());
|
UpdateBeatmapSet(beatmapSet.Detach());
|
||||||
}
|
}
|
||||||
@ -345,15 +378,20 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
private void removeBeatmapSet(Guid beatmapSetID) => Schedule(() =>
|
private void removeBeatmapSet(Guid beatmapSetID) => Schedule(() =>
|
||||||
{
|
{
|
||||||
if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSet))
|
if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSets))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach (var beatmap in existingSet.Beatmaps)
|
originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSetID);
|
||||||
randomSelectedBeatmaps.Remove(beatmap);
|
|
||||||
|
|
||||||
previouslyVisitedRandomSets.Remove(existingSet);
|
foreach (var set in existingSets)
|
||||||
|
{
|
||||||
|
foreach (var beatmap in set.Beatmaps)
|
||||||
|
randomSelectedBeatmaps.Remove(beatmap);
|
||||||
|
previouslyVisitedRandomSets.Remove(set);
|
||||||
|
|
||||||
|
root.RemoveItem(set);
|
||||||
|
}
|
||||||
|
|
||||||
root.RemoveItem(existingSet);
|
|
||||||
itemsCache.Invalidate();
|
itemsCache.Invalidate();
|
||||||
|
|
||||||
if (!Scroll.UserScrolling)
|
if (!Scroll.UserScrolling)
|
||||||
@ -366,26 +404,63 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
Guid? previouslySelectedID = null;
|
Guid? previouslySelectedID = null;
|
||||||
|
|
||||||
|
originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSet.ID);
|
||||||
|
originalBeatmapSetsDetached.Add(beatmapSet.Detach());
|
||||||
|
|
||||||
// If the selected beatmap is about to be removed, store its ID so it can be re-selected if required
|
// If the selected beatmap is about to be removed, store its ID so it can be re-selected if required
|
||||||
if (selectedBeatmapSet?.BeatmapSet.ID == beatmapSet.ID)
|
if (selectedBeatmapSet?.BeatmapSet.ID == beatmapSet.ID)
|
||||||
previouslySelectedID = selectedBeatmap?.BeatmapInfo.ID;
|
previouslySelectedID = selectedBeatmap?.BeatmapInfo.ID;
|
||||||
|
|
||||||
var newSet = createCarouselSet(beatmapSet);
|
var removedSets = root.RemoveItemsByID(beatmapSet.ID);
|
||||||
var removedSet = root.RemoveChild(beatmapSet.ID);
|
|
||||||
|
|
||||||
// If we don't remove this here, it may remain in a hidden state until scrolled off screen.
|
foreach (var removedSet in removedSets)
|
||||||
// Doesn't really affect anything during actual user interaction, but makes testing annoying.
|
|
||||||
var removedDrawable = Scroll.FirstOrDefault(c => c.Item == removedSet);
|
|
||||||
if (removedDrawable != null)
|
|
||||||
expirePanelImmediately(removedDrawable);
|
|
||||||
|
|
||||||
if (newSet != null)
|
|
||||||
{
|
{
|
||||||
root.AddItem(newSet);
|
// If we don't remove this here, it may remain in a hidden state until scrolled off screen.
|
||||||
|
// Doesn't really affect anything during actual user interaction, but makes testing annoying.
|
||||||
|
var removedDrawable = Scroll.FirstOrDefault(c => c.Item == removedSet);
|
||||||
|
if (removedDrawable != null)
|
||||||
|
expirePanelImmediately(removedDrawable);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beatmapsSplitOut)
|
||||||
|
{
|
||||||
|
var newSets = new List<CarouselBeatmapSet>();
|
||||||
|
|
||||||
|
foreach (var beatmap in beatmapSet.Beatmaps)
|
||||||
|
{
|
||||||
|
var newSet = createCarouselSet(new BeatmapSetInfo(new[] { beatmap })
|
||||||
|
{
|
||||||
|
ID = beatmapSet.ID,
|
||||||
|
OnlineID = beatmapSet.OnlineID
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newSet != null)
|
||||||
|
{
|
||||||
|
newSets.Add(newSet);
|
||||||
|
root.AddItem(newSet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// check if we can/need to maintain our current selection.
|
// check if we can/need to maintain our current selection.
|
||||||
if (previouslySelectedID != null)
|
if (previouslySelectedID != null)
|
||||||
select((CarouselItem?)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet);
|
{
|
||||||
|
var toSelect = newSets.FirstOrDefault(s => s.Beatmaps.Any(b => b.BeatmapInfo.ID == previouslySelectedID))
|
||||||
|
?? newSets.FirstOrDefault();
|
||||||
|
select(toSelect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var newSet = createCarouselSet(beatmapSet);
|
||||||
|
|
||||||
|
if (newSet != null)
|
||||||
|
{
|
||||||
|
root.AddItem(newSet);
|
||||||
|
|
||||||
|
// check if we can/need to maintain our current selection.
|
||||||
|
if (previouslySelectedID != null)
|
||||||
|
select((CarouselItem?)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
itemsCache.Invalidate();
|
itemsCache.Invalidate();
|
||||||
@ -632,6 +707,8 @@ namespace osu.Game.Screens.Select
|
|||||||
applyActiveCriteria(debounce);
|
applyActiveCriteria(debounce);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool beatmapsSplitOut;
|
||||||
|
|
||||||
private void applyActiveCriteria(bool debounce, bool alwaysResetScrollPosition = true)
|
private void applyActiveCriteria(bool debounce, bool alwaysResetScrollPosition = true)
|
||||||
{
|
{
|
||||||
PendingFilter?.Cancel();
|
PendingFilter?.Cancel();
|
||||||
@ -652,6 +729,13 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
PendingFilter = null;
|
PendingFilter = null;
|
||||||
|
|
||||||
|
if (activeCriteria.SplitOutDifficulties != beatmapsSplitOut)
|
||||||
|
{
|
||||||
|
beatmapsSplitOut = activeCriteria.SplitOutDifficulties;
|
||||||
|
loadBeatmapSets(originalBeatmapSetsDetached);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
root.Filter(activeCriteria);
|
root.Filter(activeCriteria);
|
||||||
itemsCache.Invalidate();
|
itemsCache.Invalidate();
|
||||||
|
|
||||||
@ -1055,7 +1139,7 @@ namespace osu.Game.Screens.Select
|
|||||||
// May only be null during construction (State.Value set causes PerformSelection to be triggered).
|
// May only be null during construction (State.Value set causes PerformSelection to be triggered).
|
||||||
private readonly BeatmapCarousel? carousel;
|
private readonly BeatmapCarousel? carousel;
|
||||||
|
|
||||||
public readonly Dictionary<Guid, CarouselBeatmapSet> BeatmapSetsByID = new Dictionary<Guid, CarouselBeatmapSet>();
|
public readonly Dictionary<Guid, List<CarouselBeatmapSet>> BeatmapSetsByID = new Dictionary<Guid, List<CarouselBeatmapSet>>();
|
||||||
|
|
||||||
public CarouselRoot(BeatmapCarousel carousel)
|
public CarouselRoot(BeatmapCarousel carousel)
|
||||||
{
|
{
|
||||||
@ -1069,20 +1153,25 @@ namespace osu.Game.Screens.Select
|
|||||||
public override void AddItem(CarouselItem i)
|
public override void AddItem(CarouselItem i)
|
||||||
{
|
{
|
||||||
CarouselBeatmapSet set = (CarouselBeatmapSet)i;
|
CarouselBeatmapSet set = (CarouselBeatmapSet)i;
|
||||||
BeatmapSetsByID.Add(set.BeatmapSet.ID, set);
|
if (BeatmapSetsByID.TryGetValue(set.BeatmapSet.ID, out var sets))
|
||||||
|
sets.Add(set);
|
||||||
|
else
|
||||||
|
BeatmapSetsByID.Add(set.BeatmapSet.ID, new List<CarouselBeatmapSet> { set });
|
||||||
|
|
||||||
base.AddItem(i);
|
base.AddItem(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CarouselBeatmapSet? RemoveChild(Guid beatmapSetID)
|
public IEnumerable<CarouselBeatmapSet> RemoveItemsByID(Guid beatmapSetID)
|
||||||
{
|
{
|
||||||
if (BeatmapSetsByID.TryGetValue(beatmapSetID, out var carouselBeatmapSet))
|
if (BeatmapSetsByID.TryGetValue(beatmapSetID, out var carouselBeatmapSets))
|
||||||
{
|
{
|
||||||
RemoveItem(carouselBeatmapSet);
|
foreach (var set in carouselBeatmapSets)
|
||||||
return carouselBeatmapSet;
|
RemoveItem(set);
|
||||||
|
|
||||||
|
return carouselBeatmapSets;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return Enumerable.Empty<CarouselBeatmapSet>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void RemoveItem(CarouselItem i)
|
public override void RemoveItem(CarouselItem i)
|
||||||
|
@ -30,6 +30,7 @@ using osu.Game.Rulesets;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Resources.Localisation.Web;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Select
|
namespace osu.Game.Screens.Select
|
||||||
{
|
{
|
||||||
@ -371,7 +372,7 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
new InfoLabel(new BeatmapStatistic
|
new InfoLabel(new BeatmapStatistic
|
||||||
{
|
{
|
||||||
Name = $"Length (Drain: {playableBeatmap.CalculateDrainLength().ToFormattedDuration().ToString()})",
|
Name = BeatmapsetsStrings.ShowStatsTotalLength(playableBeatmap.CalculateDrainLength().ToFormattedDuration()),
|
||||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length),
|
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length),
|
||||||
Content = working.BeatmapInfo.Length.ToFormattedDuration().ToString(),
|
Content = working.BeatmapInfo.Length.ToFormattedDuration().ToString(),
|
||||||
}),
|
}),
|
||||||
@ -415,7 +416,7 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
bpmLabelContainer.Child = new InfoLabel(new BeatmapStatistic
|
bpmLabelContainer.Child = new InfoLabel(new BeatmapStatistic
|
||||||
{
|
{
|
||||||
Name = "BPM",
|
Name = BeatmapsetsStrings.ShowStatsBpm,
|
||||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm),
|
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm),
|
||||||
Content = labelText
|
Content = labelText
|
||||||
});
|
});
|
||||||
|
@ -19,6 +19,11 @@ namespace osu.Game.Screens.Select
|
|||||||
public GroupMode Group;
|
public GroupMode Group;
|
||||||
public SortMode Sort;
|
public SortMode Sort;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the display of beatmap sets should be split apart per-difficulty for the current criteria.
|
||||||
|
/// </summary>
|
||||||
|
public bool SplitOutDifficulties => Sort == SortMode.Difficulty;
|
||||||
|
|
||||||
public BeatmapSetInfo? SelectedBeatmapSet;
|
public BeatmapSetInfo? SelectedBeatmapSet;
|
||||||
|
|
||||||
public OptionalRange<double> StarDifficulty;
|
public OptionalRange<double> StarDifficulty;
|
||||||
|
@ -32,6 +32,9 @@ namespace osu.Game.Screens.Select.Options
|
|||||||
|
|
||||||
public override bool BlockScreenWideMouse => false;
|
public override bool BlockScreenWideMouse => false;
|
||||||
|
|
||||||
|
protected override string PopInSampleName => "SongSelect/options-pop-in";
|
||||||
|
protected override string PopOutSampleName => "SongSelect/options-pop-out";
|
||||||
|
|
||||||
public BeatmapOptionsOverlay()
|
public BeatmapOptionsOverlay()
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Y;
|
AutoSizeAxes = Axes.Y;
|
||||||
|
@ -6,14 +6,13 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Localisation;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
|
||||||
namespace osu.Game.Users.Drawables
|
namespace osu.Game.Users.Drawables
|
||||||
{
|
{
|
||||||
public partial class ClickableAvatar : OsuClickableContainer
|
public partial class ClickableAvatar : OsuClickableContainer
|
||||||
{
|
{
|
||||||
private const string default_tooltip_text = "view profile";
|
|
||||||
|
|
||||||
public override LocalisableString TooltipText
|
public override LocalisableString TooltipText
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@ -21,7 +20,7 @@ namespace osu.Game.Users.Drawables
|
|||||||
if (!Enabled.Value)
|
if (!Enabled.Value)
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
|
||||||
return ShowUsernameTooltip ? (user?.Username ?? string.Empty) : default_tooltip_text;
|
return ShowUsernameTooltip ? (user?.Username ?? string.Empty) : ContextMenuStrings.ViewProfile;
|
||||||
}
|
}
|
||||||
set => throw new NotSupportedException();
|
set => throw new NotSupportedException();
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,7 @@ namespace osu.Game.Utils
|
|||||||
|
|
||||||
options.AutoSessionTracking = true;
|
options.AutoSessionTracking = true;
|
||||||
options.IsEnvironmentUser = false;
|
options.IsEnvironmentUser = false;
|
||||||
|
options.IsGlobalModeEnabled = true;
|
||||||
// The reported release needs to match version as reported to Sentry in .github/workflows/sentry-release.yml
|
// The reported release needs to match version as reported to Sentry in .github/workflows/sentry-release.yml
|
||||||
options.Release = $"osu@{game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty)}";
|
options.Release = $"osu@{game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty)}";
|
||||||
});
|
});
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Realm" Version="11.1.2" />
|
<PackageReference Include="Realm" Version="11.1.2" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2023.822.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2023.823.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.822.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.822.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.28.1" />
|
<PackageReference Include="Sentry" Version="3.28.1" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.32.2" />
|
<PackageReference Include="SharpCompress" Version="0.32.2" />
|
||||||
|
@ -23,6 +23,6 @@
|
|||||||
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.817.0" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.823.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -170,7 +170,7 @@
|
|||||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantUsingDirective/@EntryIndexedValue">ERROR</s:String>
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantUsingDirective/@EntryIndexedValue">ERROR</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantStringInterpolation/@EntryIndexedValue">WARNING</s:String>
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantStringInterpolation/@EntryIndexedValue">WARNING</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantVerbatimPrefix/@EntryIndexedValue">WARNING</s:String>
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantVerbatimPrefix/@EntryIndexedValue">WARNING</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantVerbatimStringPrefix/@EntryIndexedValue">HINT</s:String>
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantVerbatimStringPrefix/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RemoveRedundantOrStatement_002EFalse/@EntryIndexedValue">WARNING</s:String>
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RemoveRedundantOrStatement_002EFalse/@EntryIndexedValue">WARNING</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RemoveRedundantOrStatement_002ETrue/@EntryIndexedValue">WARNING</s:String>
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RemoveRedundantOrStatement_002ETrue/@EntryIndexedValue">WARNING</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RemoveToList_002E1/@EntryIndexedValue">WARNING</s:String>
|
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RemoveToList_002E1/@EntryIndexedValue">WARNING</s:String>
|
||||||
|
Loading…
Reference in New Issue
Block a user