1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-21 03:02:54 +08:00

Merge branch 'master' into check-fail-triggerer

This commit is contained in:
Dean Herbert 2024-08-07 01:01:47 +09:00
commit 7272747553
No known key found for this signature in database
189 changed files with 4559 additions and 1497 deletions

View File

@ -21,7 +21,7 @@
] ]
}, },
"ppy.localisationanalyser.tools": { "ppy.localisationanalyser.tools": {
"version": "2024.517.0", "version": "2024.802.0",
"commands": [ "commands": [
"localisation" "localisation"
] ]

View File

@ -64,10 +64,11 @@ jobs:
matrix: matrix:
os: os:
- { prettyname: Windows, fullname: windows-latest } - { prettyname: Windows, fullname: windows-latest }
- { prettyname: macOS, fullname: macos-latest } # macOS runner performance has gotten unbearably slow so let's turn them off temporarily.
# - { prettyname: macOS, fullname: macos-latest }
- { prettyname: Linux, fullname: ubuntu-latest } - { prettyname: Linux, fullname: ubuntu-latest }
threadingMode: ['SingleThread', 'MultiThreaded'] threadingMode: ['SingleThread', 'MultiThreaded']
timeout-minutes: 60 timeout-minutes: 120
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@ -55,7 +55,7 @@ When in doubt, it's probably best to start with a discussion first. We will esca
While pull requests from unaffiliated contributors are welcome, please note that due to significant community interest and limited review throughput, the core team's primary focus is on the issues which are currently [on the roadmap](https://github.com/orgs/ppy/projects/7/views/6). Reviewing PRs that fall outside of the scope of the roadmap is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change. While pull requests from unaffiliated contributors are welcome, please note that due to significant community interest and limited review throughput, the core team's primary focus is on the issues which are currently [on the roadmap](https://github.com/orgs/ppy/projects/7/views/6). Reviewing PRs that fall outside of the scope of the roadmap is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change.
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience. The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good first issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience.
In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive. In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.702.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2024.802.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.

View File

@ -82,6 +82,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddMouseMoveStep(-100, 100); AddMouseMoveStep(-100, 100);
addVertexCheckStep(3, 1, times[0], positions[0]); addVertexCheckStep(3, 1, times[0], positions[0]);
addDragEndStep();
} }
[Test] [Test]
@ -100,6 +101,9 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddMouseMoveStep(times[2] - 50, positions[2] - 50); AddMouseMoveStep(times[2] - 50, positions[2] - 50);
addVertexCheckStep(4, 1, times[1] - 50, positions[1] - 50); addVertexCheckStep(4, 1, times[1] - 50, positions[1] - 50);
addVertexCheckStep(4, 2, times[2] - 50, positions[2] - 50); addVertexCheckStep(4, 2, times[2] - 50, positions[2] - 50);
AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft));
addDragEndStep();
} }
[Test] [Test]
@ -113,6 +117,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addDragStartStep(times[1], positions[1]); addDragStartStep(times[1], positions[1]);
AddMouseMoveStep(times[1], 400); AddMouseMoveStep(times[1], 400);
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityMultiplierBindable.IsDefault); AddAssert("slider velocity changed", () => !hitObject.SliderVelocityMultiplierBindable.IsDefault);
addDragEndStep();
} }
[Test] [Test]
@ -129,6 +134,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddStep("scroll playfield", () => manualClock.CurrentTime += 200); AddStep("scroll playfield", () => manualClock.CurrentTime += 200);
AddMouseMoveStep(times[1] + 200, positions[1] + 100); AddMouseMoveStep(times[1] + 200, positions[1] + 100);
addVertexCheckStep(2, 1, times[1] + 200, positions[1] + 100); addVertexCheckStep(2, 1, times[1] + 200, positions[1] + 100);
addDragEndStep();
} }
[Test] [Test]
@ -161,18 +167,18 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addAddVertexSteps(500, 150); addAddVertexSteps(500, 150);
addVertexCheckStep(3, 1, 500, 150); addVertexCheckStep(3, 1, 500, 150);
addAddVertexSteps(90, 200); addAddVertexSteps(160, 200);
addVertexCheckStep(4, 1, times[0], positions[0]); addVertexCheckStep(4, 1, 160, 200);
addAddVertexSteps(750, 180); addAddVertexSteps(750, 180);
addVertexCheckStep(5, 4, 750, 180); addVertexCheckStep(5, 4, 800, 160);
AddAssert("duration is changed", () => Precision.AlmostEquals(hitObject.Duration, 800 - times[0], 1e-3)); AddAssert("duration is changed", () => Precision.AlmostEquals(hitObject.Duration, 800 - times[0], 1e-3));
} }
[Test] [Test]
public void TestDeleteVertex() public void TestDeleteVertex()
{ {
double[] times = { 100, 300, 500 }; double[] times = { 100, 300, 400 };
float[] positions = { 100, 200, 150 }; float[] positions = { 100, 200, 150 };
addBlueprintStep(times, positions); addBlueprintStep(times, positions);
@ -265,7 +271,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddStep("delete vertex", () => AddStep("delete vertex", () =>
{ {
InputManager.PressKey(Key.ShiftLeft); InputManager.PressKey(Key.ShiftLeft);
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Right);
InputManager.ReleaseKey(Key.ShiftLeft); InputManager.ReleaseKey(Key.ShiftLeft);
}); });
} }

View File

@ -13,6 +13,7 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
@ -42,6 +43,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
[Resolved] [Resolved]
private IBeatSnapProvider? beatSnapProvider { get; set; } private IBeatSnapProvider? beatSnapProvider { get; set; }
[Resolved]
protected EditorBeatmap? EditorBeatmap { get; private set; }
protected EditablePath(Func<float, double> positionToTime) protected EditablePath(Func<float, double> positionToTime)
{ {
PositionToTime = positionToTime; PositionToTime = positionToTime;
@ -103,15 +107,23 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
// //
// The value is clamped here by the bindable min and max values. // The value is clamped here by the bindable min and max values.
// In case the required velocity is too large, the path is not preserved. // In case the required velocity is too large, the path is not preserved.
double previousVelocity = svBindable.Value;
svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor); svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor);
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity); // adjust velocity locally, so that once the SV change is applied by applying defaults
// (triggered by `EditorBeatmap.Update()` call at end of method),
// it results in the outcome desired by the user.
double relativeChange = svBindable.Value / previousVelocity;
double localVelocity = hitObject.Velocity * relativeChange;
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, localVelocity);
if (beatSnapProvider == null) return; if (beatSnapProvider == null) return;
double endTime = hitObject.StartTime + path.Duration; double endTime = hitObject.StartTime + path.Duration;
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime); double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity; hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * localVelocity;
EditorBeatmap?.Update(hitObject);
} }
public Vector2 ToRelativePosition(Vector2 screenSpacePosition) public Vector2 ToRelativePosition(Vector2 screenSpacePosition)

View File

@ -4,12 +4,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Edit; using osu.Game.Rulesets.Catch.Objects;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -19,22 +18,27 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{ {
public MenuItem[] ContextMenuItems => getContextMenuItems().ToArray(); public MenuItem[] ContextMenuItems => getContextMenuItems().ToArray();
private readonly JuiceStream juiceStream;
// To handle when the editor is scrolled while dragging. // To handle when the editor is scrolled while dragging.
private Vector2 dragStartPosition; private Vector2 dragStartPosition;
[Resolved] public SelectionEditablePath(JuiceStream juiceStream, Func<float, double> positionToTime)
private IEditorChangeHandler? changeHandler { get; set; }
public SelectionEditablePath(Func<float, double> positionToTime)
: base(positionToTime) : base(positionToTime)
{ {
this.juiceStream = juiceStream;
} }
public void AddVertex(Vector2 relativePosition) public void AddVertex(Vector2 relativePosition)
{ {
EditorBeatmap?.BeginChange();
double time = Math.Max(0, PositionToTime(relativePosition.Y)); double time = Math.Max(0, PositionToTime(relativePosition.Y));
int index = AddVertex(time, relativePosition.X); int index = AddVertex(time, relativePosition.X);
UpdateHitObjectFromPath(juiceStream);
selectOnly(index); selectOnly(index);
EditorBeatmap?.EndChange();
} }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => InternalChildren.Any(d => d.ReceivePositionalInputAt(screenSpacePos)); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => InternalChildren.Any(d => d.ReceivePositionalInputAt(screenSpacePos));
@ -45,9 +49,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
if (index == -1 || VertexStates[index].IsFixed) if (index == -1 || VertexStates[index].IsFixed)
return false; return false;
if (e.Button == MouseButton.Left && e.ShiftPressed) if (e.Button == MouseButton.Right && e.ShiftPressed)
{ {
EditorBeatmap?.BeginChange();
RemoveVertex(index); RemoveVertex(index);
UpdateHitObjectFromPath(juiceStream);
EditorBeatmap?.EndChange();
return true; return true;
} }
@ -74,7 +82,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
for (int i = 0; i < VertexCount; i++) for (int i = 0; i < VertexCount; i++)
VertexStates[i].VertexBeforeChange = Vertices[i]; VertexStates[i].VertexBeforeChange = Vertices[i];
changeHandler?.BeginChange(); EditorBeatmap?.BeginChange();
return true; return true;
} }
@ -88,7 +96,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
protected override void OnDragEnd(DragEndEvent e) protected override void OnDragEnd(DragEndEvent e)
{ {
changeHandler?.EndChange(); EditorBeatmap?.EndChange();
} }
private int getMouseTargetVertex(Vector2 screenSpacePosition) private int getMouseTargetVertex(Vector2 screenSpacePosition)
@ -118,11 +126,17 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
private void deleteSelectedVertices() private void deleteSelectedVertices()
{ {
EditorBeatmap?.BeginChange();
for (int i = VertexCount - 1; i >= 0; i--) for (int i = VertexCount - 1; i >= 0; i--)
{ {
if (VertexStates[i].IsSelected) if (VertexStates[i].IsSelected)
RemoveVertex(i); RemoveVertex(i);
} }
UpdateHitObjectFromPath(juiceStream);
EditorBeatmap?.EndChange();
} }
} }
} }

View File

@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osuTK; using osuTK;
@ -12,6 +13,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{ {
public partial class VertexPiece : Circle public partial class VertexPiece : Circle
{ {
private VertexState state = new VertexState();
[Resolved] [Resolved]
private OsuColour osuColour { get; set; } = null!; private OsuColour osuColour { get; set; } = null!;
@ -24,7 +27,32 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void UpdateFrom(VertexState state) public void UpdateFrom(VertexState state)
{ {
Colour = state.IsSelected ? osuColour.Yellow.Lighten(1) : osuColour.Yellow; this.state = state;
updateMarkerDisplay();
}
protected override bool OnHover(HoverEvent e)
{
updateMarkerDisplay();
return false;
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateMarkerDisplay();
}
/// <summary>
/// Updates the state of the circular control point marker.
/// </summary>
private void updateMarkerDisplay()
{
var colour = osuColour.Yellow;
if (IsHovered || state.IsSelected)
colour = colour.Lighten(1);
Colour = colour;
Alpha = state.IsFixed ? 0.5f : 1; Alpha = state.IsFixed ? 0.5f : 1;
} }
} }

View File

@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{ {
scrollingPath = new ScrollingPath(), scrollingPath = new ScrollingPath(),
nestedOutlineContainer = new NestedOutlineContainer(), nestedOutlineContainer = new NestedOutlineContainer(),
editablePath = new SelectionEditablePath(positionToTime) editablePath = new SelectionEditablePath(hitObject, positionToTime)
}; };
} }

View File

@ -15,7 +15,7 @@ using osuTK;
namespace osu.Game.Rulesets.Catch.Objects namespace osu.Game.Rulesets.Catch.Objects
{ {
public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation, IHasTimePreempt
{ {
public const float OBJECT_RADIUS = 64; public const float OBJECT_RADIUS = 64;

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override ModType Type => ModType.Conversion; public override ModType Type => ModType.Conversion;
public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert) }; public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert), typeof(ManiaModNoRelease) };
public void ApplyToBeatmap(IBeatmap beatmap) public void ApplyToBeatmap(IBeatmap beatmap)
{ {

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using osu.Framework.Localisation; using osu.Framework.Localisation;
@ -27,6 +28,8 @@ namespace osu.Game.Rulesets.Mania.Mods
public override ModType Type => ModType.DifficultyReduction; public override ModType Type => ModType.DifficultyReduction;
public override Type[] IncompatibleMods => new[] { typeof(ManiaModHoldOff) };
public void ApplyToBeatmap(IBeatmap beatmap) public void ApplyToBeatmap(IBeatmap beatmap)
{ {
var maniaBeatmap = (ManiaBeatmap)beatmap; var maniaBeatmap = (ManiaBeatmap)beatmap;

View File

@ -268,11 +268,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
ApplyMaxResult(); ApplyMaxResult();
else else
MissForcefully(); MissForcefully();
}
// Make sure that the hold note is fully judged by giving the body a judgement. // Make sure that the hold note is fully judged by giving the body a judgement.
if (Tail.AllJudged && !Body.AllJudged) if (!Body.AllJudged)
Body.TriggerResult(Tail.IsHit); Body.TriggerResult(Tail.IsHit);
// Important that this is always called when a result is applied.
endHold();
}
} }
public override void MissForcefully() public override void MissForcefully()

View File

@ -42,7 +42,12 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{ {
base.PostProcess(); base.PostProcess();
var hitObjects = Beatmap.HitObjects as List<OsuHitObject> ?? Beatmap.HitObjects.OfType<OsuHitObject>().ToList(); ApplyStacking(Beatmap);
}
internal static void ApplyStacking(IBeatmap beatmap)
{
var hitObjects = beatmap.HitObjects as List<OsuHitObject> ?? beatmap.HitObjects.OfType<OsuHitObject>().ToList();
if (hitObjects.Count > 0) if (hitObjects.Count > 0)
{ {
@ -50,14 +55,14 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
foreach (var h in hitObjects) foreach (var h in hitObjects)
h.StackHeight = 0; h.StackHeight = 0;
if (Beatmap.BeatmapInfo.BeatmapVersion >= 6) if (beatmap.BeatmapInfo.BeatmapVersion >= 6)
applyStacking(Beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1); applyStacking(beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1);
else else
applyStackingOld(Beatmap.BeatmapInfo, hitObjects); applyStackingOld(beatmap.BeatmapInfo, hitObjects);
} }
} }
private void applyStacking(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects, int startIndex, int endIndex) private static void applyStacking(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects, int startIndex, int endIndex)
{ {
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex); ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
ArgumentOutOfRangeException.ThrowIfNegative(startIndex); ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
@ -209,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
} }
} }
private void applyStackingOld(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects) private static void applyStackingOld(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects)
{ {
for (int i = 0; i < hitObjects.Count; i++) for (int i = 0; i < hitObjects.Count; i++)
{ {

View File

@ -295,6 +295,12 @@ namespace osu.Game.Rulesets.Osu.Edit
if (Vector2.Distance(closestSnapPosition, screenSpacePosition) < snapRadius) if (Vector2.Distance(closestSnapPosition, screenSpacePosition) < snapRadius)
{ {
// if the snap target is a stacked object, snap to its unstacked position rather than its stacked position.
// this is intended to make working with stacks easier (because thanks to this, you can drag an object to any
// of the items on the stack to add an object to it, rather than having to drag to the position of the *first* object on it at all times).
if (b.Item is OsuHitObject osuObject && osuObject.StackOffset != Vector2.Zero)
closestSnapPosition = b.ToScreenSpace(b.ToLocalSpace(closestSnapPosition) - osuObject.StackOffset);
// only return distance portion, since time is not really valid // only return distance portion, since time is not really valid
snapResult = new SnapResult(closestSnapPosition, null, playfield); snapResult = new SnapResult(closestSnapPosition, null, playfield);
return true; return true;

View File

@ -13,6 +13,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
@ -50,12 +51,33 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
var hitObjects = selectedMovableObjects; var hitObjects = selectedMovableObjects;
var localDelta = this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta);
// this conditional is a rather ugly special case for stacks.
// as it turns out, adding the `EditorBeatmap.Update()` call at the end of this would cause stacked objects to jitter when moved around
// (they would stack and then unstack every frame).
// the reason for that is that the selection handling abstractions are not aware of the distinction between "displayed" and "actual" position
// which is unique to osu! due to stacking being applied as a post-processing step.
// therefore, the following loop would occur:
// - on frame 1 the blueprint is snapped to the stack's baseline position. `EditorBeatmap.Update()` applies stacking successfully,
// the blueprint moves up the stack from its original drag position.
// - on frame 2 the blueprint's position is now the *stacked* position, which is interpreted higher up as *manually performing an unstack*
// to the blueprint's unstacked position (as the machinery higher up only cares about differences in screen space position).
if (hitObjects.Any(h => Precision.AlmostEquals(localDelta, -h.StackOffset)))
return true;
// this will potentially move the selection out of bounds... // this will potentially move the selection out of bounds...
foreach (var h in hitObjects) foreach (var h in hitObjects)
h.Position += this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta); h.Position += localDelta;
// but this will be corrected. // but this will be corrected.
moveSelectionInBounds(); moveSelectionInBounds();
// manually update stacking.
// this intentionally bypasses the editor `UpdateState()` / beatmap processor flow for performance reasons,
// as the entire flow is too expensive to run on every movement.
Scheduler.AddOnce(OsuBeatmapProcessor.ApplyStacking, EditorBeatmap);
return true; return true;
} }

View File

@ -5,19 +5,23 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public partial class DrawableOsuJudgement : DrawableJudgement public partial class DrawableOsuJudgement : DrawableJudgement
{ {
internal Color4 AccentColour { get; private set; }
internal SkinnableLighting Lighting { get; private set; } = null!; internal SkinnableLighting Lighting { get; private set; } = null!;
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } = null!; private OsuConfigManager config { get; set; } = null!;
private bool positionTransferred; private Vector2 screenSpacePosition;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@ -32,37 +36,36 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}); });
} }
public override void Apply(JudgementResult result, DrawableHitObject? judgedObject)
{
base.Apply(result, judgedObject);
if (judgedObject is not DrawableOsuHitObject osuObject)
return;
AccentColour = osuObject.AccentColour.Value;
switch (osuObject)
{
case DrawableSlider slider:
screenSpacePosition = slider.TailCircle.ToScreenSpace(slider.TailCircle.OriginPosition);
break;
default:
screenSpacePosition = osuObject.ToScreenSpace(osuObject.OriginPosition);
break;
}
Scale = new Vector2(osuObject.HitObject.Scale);
}
protected override void PrepareForUse() protected override void PrepareForUse()
{ {
base.PrepareForUse(); base.PrepareForUse();
Lighting.ResetAnimation(); Lighting.ResetAnimation();
Lighting.SetColourFrom(JudgedObject, Result); Lighting.SetColourFrom(this, Result);
Position = Parent!.ToLocalSpace(screenSpacePosition);
positionTransferred = false;
}
protected override void Update()
{
base.Update();
if (!positionTransferred && JudgedObject is DrawableOsuHitObject osuObject && JudgedObject.IsInUse)
{
switch (osuObject)
{
case DrawableSlider slider:
Position = slider.TailCircle.ToSpaceOfOtherDrawable(slider.TailCircle.OriginPosition, Parent!);
break;
default:
Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!);
break;
}
positionTransferred = true;
Scale = new Vector2(osuObject.HitObject.Scale);
}
} }
protected override void ApplyHitAnimations() protected override void ApplyHitAnimations()

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -12,8 +10,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
internal partial class SkinnableLighting : SkinnableSprite internal partial class SkinnableLighting : SkinnableSprite
{ {
private DrawableHitObject targetObject; private DrawableOsuJudgement? targetJudgement;
private JudgementResult targetResult; private JudgementResult? targetResult;
public SkinnableLighting() public SkinnableLighting()
: base("lighting") : base("lighting")
@ -29,11 +27,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// <summary> /// <summary>
/// Updates the lighting colour from a given hitobject and result. /// Updates the lighting colour from a given hitobject and result.
/// </summary> /// </summary>
/// <param name="targetObject">The <see cref="DrawableHitObject"/> that's been judged.</param> /// <param name="targetJudgement">The <see cref="DrawableHitObject"/> that's been judged.</param>
/// <param name="targetResult">The <see cref="JudgementResult"/> that <paramref name="targetObject"/> was judged with.</param> /// <param name="targetResult">The <see cref="JudgementResult"/> that <paramref name="targetJudgement"/> was judged with.</param>
public void SetColourFrom(DrawableHitObject targetObject, JudgementResult targetResult) public void SetColourFrom(DrawableOsuJudgement targetJudgement, JudgementResult? targetResult)
{ {
this.targetObject = targetObject; this.targetJudgement = targetJudgement;
this.targetResult = targetResult; this.targetResult = targetResult;
updateColour(); updateColour();
@ -41,10 +39,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private void updateColour() private void updateColour()
{ {
if (targetObject == null || targetResult == null) if (targetJudgement == null || targetResult == null)
Colour = Color4.White; Colour = Color4.White;
else else
Colour = targetResult.IsHit ? targetObject.AccentColour.Value : Color4.Transparent; Colour = targetResult.IsHit ? targetJudgement.AccentColour : Color4.Transparent;
} }
} }
} }

View File

@ -14,7 +14,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Objects namespace osu.Game.Rulesets.Osu.Objects
{ {
public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition, IHasTimePreempt
{ {
/// <summary> /// <summary>
/// The radius of hit objects (ie. the radius of a <see cref="HitCircle"/>). /// The radius of hit objects (ie. the radius of a <see cref="HitCircle"/>).
@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary> /// </summary>
public const double PREEMPT_MAX = 1800; public const double PREEMPT_MAX = 1800;
public double TimePreempt = 600; public double TimePreempt { get; set; } = 600;
public double TimeFadeIn = 400; public double TimeFadeIn = 400;
private HitObjectProperty<Vector2> position; private HitObjectProperty<Vector2> position;

View File

@ -16,7 +16,6 @@ using osu.Framework.Graphics.Shaders.Types;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Framework.Timing; using osu.Framework.Timing;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -63,8 +62,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
// -1 signals that the part is unusable, and should not be drawn // -1 signals that the part is unusable, and should not be drawn
parts[i].InvalidationID = -1; parts[i].InvalidationID = -1;
} }
AddLayout(partSizeCache);
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -95,12 +92,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
} }
} }
private readonly LayoutValue<Vector2> partSizeCache = new LayoutValue<Vector2>(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence);
private Vector2 partSize => partSizeCache.IsValid
? partSizeCache.Value
: (partSizeCache.Value = new Vector2(Texture.DisplayWidth, Texture.DisplayHeight) * DrawInfo.Matrix.ExtractScale().Xy);
/// <summary> /// <summary>
/// The amount of time to fade the cursor trail pieces. /// The amount of time to fade the cursor trail pieces.
/// </summary> /// </summary>
@ -156,6 +147,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
protected void AddTrail(Vector2 position) protected void AddTrail(Vector2 position)
{ {
position = ToLocalSpace(position);
if (InterpolateMovements) if (InterpolateMovements)
{ {
if (!lastPosition.HasValue) if (!lastPosition.HasValue)
@ -174,7 +167,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
float distance = diff.Length; float distance = diff.Length;
Vector2 direction = diff / distance; Vector2 direction = diff / distance;
float interval = partSize.X / 2.5f * IntervalMultiplier; float interval = Texture.DisplayWidth / 2.5f * IntervalMultiplier;
float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0); float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0);
for (float d = interval; d < stopAt; d += interval) for (float d = interval; d < stopAt; d += interval)
@ -191,9 +184,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
} }
} }
private void addPart(Vector2 screenSpacePosition) private void addPart(Vector2 localSpacePosition)
{ {
parts[currentIndex].Position = screenSpacePosition; parts[currentIndex].Position = localSpacePosition;
parts[currentIndex].Time = time + 1; parts[currentIndex].Time = time + 1;
++parts[currentIndex].InvalidationID; ++parts[currentIndex].InvalidationID;
@ -220,7 +213,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private float fadeExponent; private float fadeExponent;
private readonly TrailPart[] parts = new TrailPart[max_sprites]; private readonly TrailPart[] parts = new TrailPart[max_sprites];
private Vector2 size;
private Vector2 originPosition; private Vector2 originPosition;
private IVertexBatch<TexturedTrailVertex> vertexBatch; private IVertexBatch<TexturedTrailVertex> vertexBatch;
@ -236,7 +228,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
shader = Source.shader; shader = Source.shader;
texture = Source.texture; texture = Source.texture;
size = Source.partSize;
time = Source.time; time = Source.time;
fadeExponent = Source.FadeExponent; fadeExponent = Source.FadeExponent;
@ -277,6 +268,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
RectangleF textureRect = texture.GetTextureRect(); RectangleF textureRect = texture.GetTextureRect();
renderer.PushLocalMatrix(DrawInfo.Matrix);
foreach (var part in parts) foreach (var part in parts)
{ {
if (part.InvalidationID == -1) if (part.InvalidationID == -1)
@ -287,7 +280,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex vertexBatch.Add(new TexturedTrailVertex
{ {
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)), Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomLeft, TexturePosition = textureRect.BottomLeft,
TextureRect = new Vector4(0, 0, 1, 1), TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomLeft.Linear, Colour = DrawColourInfo.Colour.BottomLeft.Linear,
@ -296,7 +289,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex vertexBatch.Add(new TexturedTrailVertex
{ {
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)), Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X), part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomRight, TexturePosition = textureRect.BottomRight,
TextureRect = new Vector4(0, 0, 1, 1), TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomRight.Linear, Colour = DrawColourInfo.Colour.BottomRight.Linear,
@ -305,7 +298,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex vertexBatch.Add(new TexturedTrailVertex
{ {
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y), Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X), part.Position.Y - texture.DisplayHeight * originPosition.Y),
TexturePosition = textureRect.TopRight, TexturePosition = textureRect.TopRight,
TextureRect = new Vector4(0, 0, 1, 1), TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopRight.Linear, Colour = DrawColourInfo.Colour.TopRight.Linear,
@ -314,7 +307,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex vertexBatch.Add(new TexturedTrailVertex
{ {
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y), Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X, part.Position.Y - texture.DisplayHeight * originPosition.Y),
TexturePosition = textureRect.TopLeft, TexturePosition = textureRect.TopLeft,
TextureRect = new Vector4(0, 0, 1, 1), TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopLeft.Linear, Colour = DrawColourInfo.Colour.TopLeft.Linear,
@ -322,6 +315,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
}); });
} }
renderer.PopLocalMatrix();
vertexBatch.Draw(); vertexBatch.Draw();
shader.Unbind(); shader.Unbind();
} }

View File

@ -206,6 +206,15 @@ namespace osu.Game.Rulesets.Osu.UI
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos);
private OsuResumeOverlay.OsuResumeOverlayInputBlocker? resumeInputBlocker;
public void AttachResumeOverlayInputBlocker(OsuResumeOverlay.OsuResumeOverlayInputBlocker resumeInputBlocker)
{
Debug.Assert(this.resumeInputBlocker == null);
this.resumeInputBlocker = resumeInputBlocker;
AddInternal(resumeInputBlocker);
}
private partial class ProxyContainer : LifetimeManagementContainer private partial class ProxyContainer : LifetimeManagementContainer
{ {
public void Add(Drawable proxy) => AddInternal(proxy); public void Add(Drawable proxy) => AddInternal(proxy);

View File

@ -33,9 +33,30 @@ namespace osu.Game.Rulesets.Osu.UI
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
OsuResumeOverlayInputBlocker? inputBlocker = null;
if (drawableRuleset != null)
{
var osuPlayfield = (OsuPlayfield)drawableRuleset.Playfield;
osuPlayfield.AttachResumeOverlayInputBlocker(inputBlocker = new OsuResumeOverlayInputBlocker());
}
Add(cursorScaleContainer = new Container Add(cursorScaleContainer = new Container
{ {
Child = clickToResumeCursor = new OsuClickToResumeCursor { ResumeRequested = Resume } Child = clickToResumeCursor = new OsuClickToResumeCursor
{
ResumeRequested = () =>
{
// since the user had to press a button to tap the resume cursor,
// block that press event from potentially reaching a hit circle that's behind the cursor.
// we cannot do this from OsuClickToResumeCursor directly since we're in a different input manager tree than the gameplay one,
// so we rely on a dedicated input blocking component that's implanted in there to do that for us.
if (inputBlocker != null)
inputBlocker.BlockNextPress = true;
Resume();
}
}
}); });
} }
@ -115,7 +136,6 @@ namespace osu.Game.Rulesets.Osu.UI
return false; return false;
scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint); scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
ResumeRequested?.Invoke(); ResumeRequested?.Invoke();
return true; return true;
} }
@ -141,5 +161,27 @@ namespace osu.Game.Rulesets.Osu.UI
this.FadeColour(IsHovered ? Color4.White : Color4.Orange, 400, Easing.OutQuint); this.FadeColour(IsHovered ? Color4.White : Color4.Orange, 400, Easing.OutQuint);
} }
} }
public partial class OsuResumeOverlayInputBlocker : Drawable, IKeyBindingHandler<OsuAction>
{
public bool BlockNextPress;
public OsuResumeOverlayInputBlocker()
{
RelativeSizeAxes = Axes.Both;
Depth = float.MinValue;
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
{
bool block = BlockNextPress;
BlockNextPress = false;
return block;
}
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
}
}
} }
} }

View File

@ -19,6 +19,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
<ProjectReference Include="..\osu.Game.Tests\osu.Game.Tests.csproj" />
<ProjectReference Include="..\osu.Game\osu.Game.csproj" /> <ProjectReference Include="..\osu.Game\osu.Game.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,70 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Storyboards;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Taiko.Tests.Editor
{
public partial class TestSceneTaikoEditorTestGameplay : EditorTestScene
{
protected override bool IsolateSavingFromDatabase => false;
protected override Ruleset CreateEditorRuleset() => new TaikoRuleset();
[Resolved]
private OsuGameBase game { get; set; } = null!;
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
private BeatmapSetInfo importedBeatmapSet = null!;
public override void SetUpSteps()
{
AddStep("import test beatmap", () => importedBeatmapSet = BeatmapImportHelper.LoadOszIntoOsu(game).GetResultSafely());
base.SetUpSteps();
}
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
=> beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 1));
[Test]
public void TestBasicGameplayTest()
{
AddStep("add objects", () =>
{
EditorBeatmap.Clear();
EditorBeatmap.Add(new Swell { StartTime = 500, EndTime = 1500 });
EditorBeatmap.Add(new Hit { StartTime = 3000 });
});
AddStep("seek to 250", () => EditorClock.Seek(250));
AddUntilStep("wait for seek", () => EditorClock.CurrentTime, () => Is.EqualTo(250));
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
AddStep("save changes", () => DialogOverlay.CurrentDialog!.PerformOkAction());
AddUntilStep("player pushed", () => Stack.CurrentScreen is EditorPlayer);
AddUntilStep("wait for return to editor", () => Stack.CurrentScreen is Screens.Edit.Editor);
}
}
}

View File

@ -315,10 +315,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
hitObjectContainer.Add(drawableSwell); hitObjectContainer.Add(drawableSwell);
}); });
// You might think that this should be a SwellTick since we're before the swell, but SwellTicks get no StartTime (ie. they are zero). AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
// But for sample playback purposes they can be ignored as noise.
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
@ -352,10 +349,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
hitObjectContainer.Add(drawableSwell); hitObjectContainer.Add(drawableSwell);
}); });
// You might think that this should be a SwellTick since we're before the swell, but SwellTicks get no StartTime (ie. they are zero). AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
// But for sample playback purposes they can be ignored as noise.
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM); checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM); checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);

View File

@ -11,5 +11,6 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Project References"> <ItemGroup Label="Project References">
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
<ProjectReference Include="..\osu.Game.Tests\osu.Game.Tests.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
AddNested(new SwellTick AddNested(new SwellTick
{ {
StartTime = StartTime,
Samples = Samples Samples = Samples
}); });
} }

View File

@ -157,8 +157,9 @@ namespace osu.Game.Tests.Database
AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False); AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False);
} }
[Test] [TestCase(30000002)]
public void TestScoreUpgradeFailed() [TestCase(30000013)]
public void TestScoreUpgradeFailed(int scoreVersion)
{ {
ScoreInfo scoreInfo = null!; ScoreInfo scoreInfo = null!;
@ -172,16 +173,18 @@ namespace osu.Game.Tests.Database
Ruleset = r.All<RulesetInfo>().First(), Ruleset = r.All<RulesetInfo>().First(),
}) })
{ {
TotalScoreVersion = 30000002, TotalScoreVersion = scoreVersion,
IsLegacyScore = true, IsLegacyScore = true,
}); });
}); });
}); });
AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor())); TestBackgroundDataStoreProcessor processor = null!;
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
AddUntilStep("Wait for completion", () => processor.Completed);
AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True); AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True);
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000002)); AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(scoreVersion));
} }
[Test] [Test]

View File

@ -259,6 +259,44 @@ namespace osu.Game.Tests.Database
}); });
} }
[Test]
public void TestNoChangesAfterDelete()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
var importer = new BeatmapImporter(storage, realm);
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchive(out string pathOriginalSecond);
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
importBeforeUpdate!.PerformWrite(s => s.DeletePending = true);
var dateBefore = importBeforeUpdate.Value.DateAdded;
Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null);
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginalSecond), importBeforeUpdate.Value);
realm.Run(r => r.Refresh());
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
checkCount<BeatmapSetInfo>(realm, 1);
checkCount<BeatmapInfo>(realm, count_beatmaps);
checkCount<BeatmapMetadata>(realm, count_beatmaps);
Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1));
Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
Assert.That(importAfterUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
Assert.That(importBeforeUpdate.ID, Is.EqualTo(importAfterUpdate.ID));
});
}
[Test] [Test]
public void TestNoChanges() public void TestNoChanges()
{ {
@ -272,21 +310,25 @@ namespace osu.Game.Tests.Database
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal)); var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
var dateBefore = importBeforeUpdate!.Value.DateAdded;
Assert.That(importBeforeUpdate, Is.Not.Null); Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null); Debug.Assert(importBeforeUpdate != null);
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginalSecond), importBeforeUpdate.Value); var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginalSecond), importBeforeUpdate.Value);
realm.Run(r => r.Refresh());
Assert.That(importAfterUpdate, Is.Not.Null); Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null); Debug.Assert(importAfterUpdate != null);
realm.Run(r => r.Refresh());
checkCount<BeatmapSetInfo>(realm, 1); checkCount<BeatmapSetInfo>(realm, 1);
checkCount<BeatmapInfo>(realm, count_beatmaps); checkCount<BeatmapInfo>(realm, count_beatmaps);
checkCount<BeatmapMetadata>(realm, count_beatmaps); checkCount<BeatmapMetadata>(realm, count_beatmaps);
Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1)); Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1));
Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
Assert.That(importAfterUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
Assert.That(importBeforeUpdate.ID, Is.EqualTo(importAfterUpdate.ID)); Assert.That(importBeforeUpdate.ID, Is.EqualTo(importAfterUpdate.ID));
}); });
} }
@ -479,6 +521,7 @@ namespace osu.Game.Tests.Database
using var rulesets = new RealmRulesetStore(realm, storage); using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal); using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory => using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory =>
{ {
// arbitrary beatmap removal // arbitrary beatmap removal
@ -496,7 +539,7 @@ namespace osu.Game.Tests.Database
Debug.Assert(importAfterUpdate != null); Debug.Assert(importAfterUpdate != null);
Assert.That(importBeforeUpdate.ID, Is.Not.EqualTo(importAfterUpdate.ID)); Assert.That(importBeforeUpdate.ID, Is.Not.EqualTo(importAfterUpdate.ID));
Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(importAfterUpdate.Value.DateAdded)); Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(importAfterUpdate.Value.DateAdded).Within(TimeSpan.FromSeconds(1)));
}); });
} }

View File

@ -25,6 +25,9 @@ namespace osu.Game.Tests.Editing
new object?[] { "1:02:3000", false, null, null }, new object?[] { "1:02:3000", false, null, null },
new object?[] { "1:02:300 ()", false, null, null }, new object?[] { "1:02:300 ()", false, null, null },
new object?[] { "1:02:300 (1,2,3)", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" }, new object?[] { "1:02:300 (1,2,3)", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" },
new object?[] { "1:02:300 (1,2,3) - ", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" },
new object?[] { "1:02:300 (1,2,3) - following mod", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" },
new object?[] { "1:02:300 (1,2,3) - following mod\nwith newlines", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" },
}; };
[TestCaseSource(nameof(test_cases))] [TestCaseSource(nameof(test_cases))]

View File

@ -45,7 +45,7 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new Note { StartTime = 1000 },
} }
}); });
@ -67,8 +67,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new Note { StartTime = 1000 },
new HitCircle { StartTime = 2000 }, new Note { StartTime = 2000 },
} }
}); });
@ -136,8 +136,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new Note { StartTime = 1000 },
new HitCircle { StartTime = 5000 }, new Note { StartTime = 5000 },
} }
}); });
@ -164,8 +164,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new Note { StartTime = 1000 },
new HitCircle { StartTime = 9000 }, new Note { StartTime = 9000 },
}, },
Breaks = Breaks =
{ {
@ -197,9 +197,9 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new Note { StartTime = 1000 },
new HitCircle { StartTime = 5000 }, new Note { StartTime = 5000 },
new HitCircle { StartTime = 9000 }, new Note { StartTime = 9000 },
}, },
Breaks = Breaks =
{ {
@ -232,8 +232,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1100 }, new Note { StartTime = 1100 },
new HitCircle { StartTime = 9000 }, new Note { StartTime = 9000 },
}, },
Breaks = Breaks =
{ {
@ -264,8 +264,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new Note { StartTime = 1000 },
new HitCircle { StartTime = 9000 }, new Note { StartTime = 9000 },
}, },
Breaks = Breaks =
{ {
@ -299,9 +299,9 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new Note { StartTime = 1000 },
new HitCircle { StartTime = 5000 }, new Note { StartTime = 5000 },
new HitCircle { StartTime = 9000 }, new Note { StartTime = 9000 },
}, },
Breaks = Breaks =
{ {
@ -334,8 +334,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new Note { StartTime = 1000 },
new HitCircle { StartTime = 9000 }, new Note { StartTime = 9000 },
}, },
Breaks = Breaks =
{ {
@ -366,8 +366,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new Note { StartTime = 1000 },
new HitCircle { StartTime = 2000 }, new Note { StartTime = 2000 },
}, },
Breaks = Breaks =
{ {
@ -393,8 +393,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 1000 }, new Note { StartTime = 1000 },
new HitCircle { StartTime = 2000 }, new Note { StartTime = 2000 },
}, },
Breaks = Breaks =
{ {
@ -447,8 +447,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 10000 }, new Note { StartTime = 10000 },
new HitCircle { StartTime = 11000 }, new Note { StartTime = 11000 },
}, },
Breaks = Breaks =
{ {
@ -474,8 +474,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }, BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects = HitObjects =
{ {
new HitCircle { StartTime = 10000 }, new Note { StartTime = 10000 },
new HitCircle { StartTime = 11000 }, new Note { StartTime = 11000 },
}, },
Breaks = Breaks =
{ {
@ -489,5 +489,55 @@ namespace osu.Game.Tests.Editing
Assert.That(beatmap.Breaks, Is.Empty); Assert.That(beatmap.Breaks, Is.Empty);
} }
[Test]
public void TestTimePreemptIsRespected()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
Difficulty =
{
ApproachRate = 10,
},
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 5000 },
}
});
foreach (var ho in beatmap.HitObjects)
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.Multiple(() =>
{
Assert.That(beatmap.Breaks, Has.Count.EqualTo(1));
Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200));
Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MIN));
});
beatmap.Difficulty.ApproachRate = 0;
foreach (var ho in beatmap.HitObjects)
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.Multiple(() =>
{
Assert.That(beatmap.Breaks, Has.Count.EqualTo(1));
Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200));
Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MAX));
});
}
} }
} }

View File

@ -8,6 +8,8 @@ using osu.Game.Online.API;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
namespace osu.Game.Tests.Mods namespace osu.Game.Tests.Mods
@ -105,9 +107,6 @@ namespace osu.Game.Tests.Mods
testMod.ResetSettingsToDefaults(); testMod.ResetSettingsToDefaults();
Assert.That(testMod.DrainRate.Value, Is.Null); Assert.That(testMod.DrainRate.Value, Is.Null);
// ReSharper disable once HeuristicUnreachableCode
// see https://youtrack.jetbrains.com/issue/RIDER-70159.
Assert.That(testMod.OverallDifficulty.Value, Is.Null); Assert.That(testMod.OverallDifficulty.Value, Is.Null);
var applied = applyDifficulty(new BeatmapDifficulty var applied = applyDifficulty(new BeatmapDifficulty
@ -119,6 +118,48 @@ namespace osu.Game.Tests.Mods
Assert.That(applied.OverallDifficulty, Is.EqualTo(10)); Assert.That(applied.OverallDifficulty, Is.EqualTo(10));
} }
[Test]
public void TestDeserializeIncorrectRange()
{
var apiMod = new APIMod
{
Acronym = @"DA",
Settings = new Dictionary<string, object>
{
[@"circle_size"] = -727,
[@"approach_rate"] = -727,
}
};
var ruleset = new OsuRuleset();
var mod = (OsuModDifficultyAdjust)apiMod.ToMod(ruleset);
Assert.Multiple(() =>
{
Assert.That(mod.CircleSize.Value, Is.GreaterThanOrEqualTo(0).And.LessThanOrEqualTo(11));
Assert.That(mod.ApproachRate.Value, Is.GreaterThanOrEqualTo(-10).And.LessThanOrEqualTo(11));
});
}
[Test]
public void TestDeserializeNegativeApproachRate()
{
var apiMod = new APIMod
{
Acronym = @"DA",
Settings = new Dictionary<string, object>
{
[@"approach_rate"] = -9,
}
};
var ruleset = new OsuRuleset();
var mod = (OsuModDifficultyAdjust)apiMod.ToMod(ruleset);
Assert.That(mod.ApproachRate.Value, Is.GreaterThanOrEqualTo(-10).And.LessThanOrEqualTo(11));
Assert.That(mod.ApproachRate.Value, Is.EqualTo(-9));
}
/// <summary> /// <summary>
/// Applies a <see cref="BeatmapDifficulty"/> to the mod and returns a new <see cref="BeatmapDifficulty"/> /// Applies a <see cref="BeatmapDifficulty"/> to the mod and returns a new <see cref="BeatmapDifficulty"/>
/// representing the result if the mod were applied to a fresh <see cref="BeatmapDifficulty"/> instance. /// representing the result if the mod were applied to a fresh <see cref="BeatmapDifficulty"/> instance.

View File

@ -4,16 +4,34 @@
using System; using System;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Metadata;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.Metadata;
using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.DailyChallenge namespace osu.Game.Tests.Visual.DailyChallenge
{ {
public partial class TestSceneDailyChallenge : OnlinePlayTestScene public partial class TestSceneDailyChallenge : OnlinePlayTestScene
{ {
[Cached(typeof(MetadataClient))]
private TestMetadataClient metadataClient = new TestMetadataClient();
[Cached(typeof(INotificationOverlay))]
private NotificationOverlay notificationOverlay = new NotificationOverlay();
[BackgroundDependencyLoader]
private void load()
{
base.Content.Add(notificationOverlay);
base.Content.Add(metadataClient);
}
[Test] [Test]
public void TestDailyChallenge() public void TestDailyChallenge()
{ {
@ -36,5 +54,33 @@ namespace osu.Game.Tests.Visual.DailyChallenge
AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
} }
[Test]
public void TestNotifications()
{
var room = new Room
{
RoomID = { Value = 1234 },
Name = { Value = "Daily Challenge: June 4, 2024" },
Playlist =
{
new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First())
{
RequiredMods = [new APIMod(new OsuModTraceable())],
AllowedMods = [new APIMod(new OsuModDoubleTime())]
}
},
EndDate = { Value = DateTimeOffset.Now.AddHours(12) },
Category = { Value = RoomCategory.DailyChallenge }
};
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null);
}
} }
} }

View File

@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -17,14 +18,14 @@ namespace osu.Game.Tests.Visual.DailyChallenge
{ {
public partial class TestSceneDailyChallengeEventFeed : OsuTestScene public partial class TestSceneDailyChallengeEventFeed : OsuTestScene
{ {
private DailyChallengeEventFeed feed = null!;
[Cached] [Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
[Test] [SetUpSteps]
public void TestBasicAppearance() public void SetUpSteps()
{ {
DailyChallengeEventFeed feed = null!;
AddStep("create content", () => Children = new Drawable[] AddStep("create content", () => Children = new Drawable[]
{ {
new Box new Box
@ -35,22 +36,28 @@ namespace osu.Game.Tests.Visual.DailyChallenge
feed = new DailyChallengeEventFeed feed = new DailyChallengeEventFeed
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Height = 0.3f,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
} }
}); });
AddSliderStep("adjust width", 0.1f, 1, 1, width => AddSliderStep("adjust width", 0.1f, 1, 1, width =>
{ {
if (feed.IsNotNull()) if (feed.IsNotNull())
feed.Width = width; feed.Width = width;
}); });
AddSliderStep("adjust height", 0.1f, 1, 1, height => AddSliderStep("adjust height", 0.1f, 1, 0.3f, height =>
{ {
if (feed.IsNotNull()) if (feed.IsNotNull())
feed.Height = height; feed.Height = height;
}); });
}
AddStep("add normal score", () => [Test]
public void TestBasicAppearance()
{
AddRepeatStep("add normal score", () =>
{ {
var ev = new NewScoreEvent(1, new APIUser var ev = new NewScoreEvent(1, new APIUser
{ {
@ -60,9 +67,9 @@ namespace osu.Game.Tests.Visual.DailyChallenge
}, RNG.Next(1_000_000), null); }, RNG.Next(1_000_000), null);
feed.AddNewScore(ev); feed.AddNewScore(ev);
}); }, 50);
AddStep("add new user best", () => AddRepeatStep("add new user best", () =>
{ {
var ev = new NewScoreEvent(1, new APIUser var ev = new NewScoreEvent(1, new APIUser
{ {
@ -75,9 +82,9 @@ namespace osu.Game.Tests.Visual.DailyChallenge
testScore.TotalScore = RNG.Next(1_000_000); testScore.TotalScore = RNG.Next(1_000_000);
feed.AddNewScore(ev); feed.AddNewScore(ev);
}); }, 50);
AddStep("add top 10 score", () => AddRepeatStep("add top 10 score", () =>
{ {
var ev = new NewScoreEvent(1, new APIUser var ev = new NewScoreEvent(1, new APIUser
{ {
@ -87,6 +94,25 @@ namespace osu.Game.Tests.Visual.DailyChallenge
}, RNG.Next(1_000_000), RNG.Next(1, 10)); }, RNG.Next(1_000_000), RNG.Next(1, 10));
feed.AddNewScore(ev); feed.AddNewScore(ev);
}, 50);
}
[Test]
public void TestMassAdd()
{
AddStep("add 1000 scores at once", () =>
{
for (int i = 0; i < 1000; i++)
{
var ev = new NewScoreEvent(1, new APIUser
{
Id = 2,
Username = "peppy",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, RNG.Next(1_000_000), null);
feed.AddNewScore(ev);
}
}); });
} }
} }

View File

@ -6,8 +6,10 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.DailyChallenge; using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events; using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
@ -19,11 +21,11 @@ namespace osu.Game.Tests.Visual.DailyChallenge
[Cached] [Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
[Test] private DailyChallengeScoreBreakdown breakdown = null!;
public void TestBasicAppearance()
{
DailyChallengeScoreBreakdown breakdown = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create content", () => Children = new Drawable[] AddStep("create content", () => Children = new Drawable[]
{ {
new Box new Box
@ -49,7 +51,14 @@ namespace osu.Game.Tests.Visual.DailyChallenge
breakdown.Height = height; breakdown.Height = height;
}); });
AddToggleStep("toggle visible", v => breakdown.Alpha = v ? 1 : 0);
AddStep("set initial data", () => breakdown.SetInitialCounts([1, 4, 9, 16, 25, 36, 49, 36, 25, 16, 9, 4, 1])); AddStep("set initial data", () => breakdown.SetInitialCounts([1, 4, 9, 16, 25, 36, 49, 36, 25, 16, 9, 4, 1]));
}
[Test]
public void TestBasicAppearance()
{
AddStep("add new score", () => AddStep("add new score", () =>
{ {
var ev = new NewScoreEvent(1, new APIUser var ev = new NewScoreEvent(1, new APIUser
@ -61,6 +70,27 @@ namespace osu.Game.Tests.Visual.DailyChallenge
breakdown.AddNewScore(ev); breakdown.AddNewScore(ev);
}); });
AddStep("set user score", () => breakdown.UserBestScore.Value = new MultiplayerScore { TotalScore = RNG.Next(1_000_000) });
AddStep("unset user score", () => breakdown.UserBestScore.Value = null);
}
[Test]
public void TestMassAdd()
{
AddStep("add 1000 scores at once", () =>
{
for (int i = 0; i < 1000; i++)
{
var ev = new NewScoreEvent(1, new APIUser
{
Id = 2,
Username = "peppy",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, RNG.Next(1_000_000), null);
breakdown.AddNewScore(ev);
}
});
} }
} }
} }

View File

@ -55,6 +55,8 @@ namespace osu.Game.Tests.Visual.DailyChallenge
if (ring.IsNotNull()) if (ring.IsNotNull())
ring.Height = height; ring.Height = height;
}); });
AddToggleStep("toggle visible", v => ring.Alpha = v ? 1 : 0);
AddStep("just started", () => AddStep("just started", () =>
{ {
room.Value.StartDate.Value = DateTimeOffset.Now.AddMinutes(-1); room.Value.StartDate.Value = DateTimeOffset.Now.AddMinutes(-1);

View File

@ -0,0 +1,87 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.DailyChallenge
{
public partial class TestSceneDailyChallengeTotalsDisplay : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
[Test]
public void TestBasicAppearance()
{
DailyChallengeTotalsDisplay totals = null!;
AddStep("create content", () => Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
totals = new DailyChallengeTotalsDisplay
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
});
AddSliderStep("adjust width", 0.1f, 1, 1, width =>
{
if (totals.IsNotNull())
totals.Width = width;
});
AddSliderStep("adjust height", 0.1f, 1, 1, height =>
{
if (totals.IsNotNull())
totals.Height = height;
});
AddToggleStep("toggle visible", v => totals.Alpha = v ? 1 : 0);
AddStep("set counts", () => totals.SetInitialCounts(totalPassCount: 9650, cumulativeTotalScore: 10_000_000_000));
AddStep("add normal score", () =>
{
var ev = new NewScoreEvent(1, new APIUser
{
Id = 2,
Username = "peppy",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, RNG.Next(1_000_000), null);
totals.AddNewScore(ev);
});
AddStep("spam scores", () =>
{
for (int i = 0; i < 1000; ++i)
{
var ev = new NewScoreEvent(1, new APIUser
{
Id = 2,
Username = "peppy",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, RNG.Next(1_000_000), RNG.Next(11, 1000));
var testScore = TestResources.CreateTestScoreInfo();
testScore.TotalScore = RNG.Next(1_000_000);
totals.AddNewScore(ev);
}
});
}
}
}

View File

@ -402,6 +402,70 @@ namespace osu.Game.Tests.Visual.Editing
void checkPlacementSample(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First().Bank, () => Is.EqualTo(expected)); void checkPlacementSample(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First().Bank, () => Is.EqualTo(expected));
} }
[Test]
public void PopoverForMultipleSelectionChangesAllSamples()
{
AddStep("add slider", () =>
{
EditorBeatmap.Add(new Slider
{
Position = new Vector2(256, 256),
StartTime = 1000,
Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }),
Samples =
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
},
NodeSamples = new List<IList<HitSampleInfo>>
{
new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_DRUM),
new HitSampleInfo(HitSampleInfo.HIT_CLAP, bank: HitSampleInfo.BANK_DRUM),
},
new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT),
new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT),
},
}
});
});
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickSamplePiece(0);
setBankViaPopover(HitSampleInfo.BANK_DRUM);
samplePopoverHasSingleBank(HitSampleInfo.BANK_DRUM);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_DRUM);
hitObjectHasSampleNormalBank(2, HitSampleInfo.BANK_DRUM);
hitObjectNodeHasSampleNormalBank(2, 0, HitSampleInfo.BANK_DRUM);
hitObjectNodeHasSampleNormalBank(2, 1, HitSampleInfo.BANK_DRUM);
setVolumeViaPopover(30);
samplePopoverHasSingleVolume(30);
hitObjectHasSampleVolume(0, 30);
hitObjectHasSampleVolume(1, 30);
hitObjectHasSampleVolume(2, 30);
hitObjectNodeHasSampleVolume(2, 0, 30);
hitObjectNodeHasSampleVolume(2, 1, 30);
toggleAdditionViaPopover(0);
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
hitObjectHasSamples(2, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
hitObjectNodeHasSamples(2, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_WHISTLE);
hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
setAdditionBankViaPopover(HitSampleInfo.BANK_SOFT);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleAdditionBank(2, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSampleAdditionBank(2, 0, HitSampleInfo.BANK_SOFT);
hitObjectNodeHasSampleAdditionBank(2, 1, HitSampleInfo.BANK_SOFT);
}
[Test] [Test]
public void TestHotkeysAffectNodeSamples() public void TestHotkeysAffectNodeSamples()
{ {

View File

@ -28,6 +28,74 @@ namespace osu.Game.Tests.Visual.Editing
private GlobalActionContainer globalActionContainer => this.ChildrenOfType<GlobalActionContainer>().Single(); private GlobalActionContainer globalActionContainer => this.ChildrenOfType<GlobalActionContainer>().Single();
[Test]
public void TestDeleteUsingMiddleMouse()
{
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items);
AddStep("delete with middle mouse", () => InputManager.Click(MouseButton.Middle));
AddAssert("circle removed", () => EditorBeatmap.HitObjects, () => Is.Empty);
}
[Test]
public void TestDeleteUsingShiftRightClick()
{
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items);
AddStep("delete with right mouse", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Click(MouseButton.Right);
InputManager.ReleaseKey(Key.ShiftLeft);
});
AddAssert("circle removed", () => EditorBeatmap.HitObjects, () => Is.Empty);
}
[Test]
public void TestContextMenu()
{
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items);
AddStep("delete with right mouse", () =>
{
InputManager.Click(MouseButton.Right);
});
AddAssert("circle not removed", () => EditorBeatmap.HitObjects, () => Has.One.Items);
AddAssert("circle selected", () => EditorBeatmap.SelectedHitObjects, () => Has.One.Items);
}
[Test]
[Solo]
public void TestCommitPlacementViaRightClick()
{
Playfield playfield = null!;
AddStep("select slider placement tool", () => InputManager.Key(Key.Number3));
AddStep("move mouse to top left of playfield", () =>
{
playfield = this.ChildrenOfType<Playfield>().Single();
var location = (3 * playfield.ScreenSpaceDrawQuad.TopLeft + playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
InputManager.MoveMouseTo(location);
});
AddStep("begin placement", () => InputManager.Click(MouseButton.Left));
AddStep("move mouse to bottom right of playfield", () =>
{
var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
InputManager.MoveMouseTo(location);
});
AddStep("confirm via right click", () => InputManager.Click(MouseButton.Right));
AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
}
[Test] [Test]
public void TestCommitPlacementViaGlobalAction() public void TestCommitPlacementViaGlobalAction()
{ {

View File

@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("set preview time to -1", () => EditorBeatmap.PreviewTime.Value = -1); AddStep("set preview time to -1", () => EditorBeatmap.PreviewTime.Value = -1);
AddAssert("preview time line should not show", () => !Editor.ChildrenOfType<PreviewTimePart>().Single().Children.Any()); AddAssert("preview time line should not show", () => !Editor.ChildrenOfType<PreviewTimePart>().Single().Children.Any());
AddStep("set preview time to 1000", () => EditorBeatmap.PreviewTime.Value = 1000); AddStep("set preview time to 1000", () => EditorBeatmap.PreviewTime.Value = 1000);
AddAssert("preview time line should show", () => Editor.ChildrenOfType<PreviewTimePart>().Single().Children.Single().Alpha == 1); AddAssert("preview time line should show", () => Editor.ChildrenOfType<PreviewTimePart>().Single().Children.Single().Alpha, () => Is.GreaterThan(0));
} }
} }
} }

View File

@ -99,6 +99,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Scheduler.AddDelayed(applyMiss, 500 + 30); Scheduler.AddDelayed(applyMiss, 500 + 30);
}); });
AddUntilStep("wait for sequence", () => !Scheduler.HasPendingTasks);
} }
[Test] [Test]
@ -120,6 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
} }
}); });
AddUntilStep("wait for sequence", () => !Scheduler.HasPendingTasks);
} }
[Test] [Test]

View File

@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Gameplay
return true; return true;
}); });
AddAssert("sample playback disabled", () => sampleDisabler.SamplePlaybackDisabled.Value); AddUntilStep("sample playback disabled", () => sampleDisabler.SamplePlaybackDisabled.Value);
// because we are in frame stable context, it's quite likely that not all samples are "played" at this point. // because we are in frame stable context, it's quite likely that not all samples are "played" at this point.
// the important thing is that at least one started, and that sample has since stopped. // the important thing is that at least one started, and that sample has since stopped.

View File

@ -0,0 +1,98 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneJudgementContainer : OsuTestScene
{
private JudgementContainer<DrawableOsuJudgement> judgementContainer = null!;
[SetUpSteps]
public void SetUp()
{
AddStep("create judgement container", () => Child = judgementContainer = new JudgementContainer<DrawableOsuJudgement>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
});
}
[Test]
public void TestJudgementFromSameHitObjectIsRemoved()
{
DrawableHitCircle drawableHitCircle1 = null!;
DrawableHitCircle drawableHitCircle2 = null!;
AddStep("create hit circles", () =>
{
Add(drawableHitCircle1 = new DrawableHitCircle(createHitCircle()));
Add(drawableHitCircle2 = new DrawableHitCircle(createHitCircle()));
});
int judgementCount = 0;
AddStep("judge the same hitobject twice via different drawables", () =>
{
addDrawableJudgement(drawableHitCircle1);
drawableHitCircle2.Apply(drawableHitCircle1.HitObject);
addDrawableJudgement(drawableHitCircle2);
judgementCount = judgementContainer.Count;
});
AddAssert("one judgement in container", () => judgementCount, () => Is.EqualTo(1));
}
[Test]
public void TestJudgementFromDifferentHitObjectIsNotRemoved()
{
DrawableHitCircle drawableHitCircle = null!;
AddStep("create hit circle", () => Add(drawableHitCircle = new DrawableHitCircle(createHitCircle())));
int judgementCount = 0;
AddStep("judge two hitobjects via the same drawable", () =>
{
addDrawableJudgement(drawableHitCircle);
drawableHitCircle.Apply(createHitCircle());
addDrawableJudgement(drawableHitCircle);
judgementCount = judgementContainer.Count;
});
AddAssert("two judgements in container", () => judgementCount, () => Is.EqualTo(2));
}
private void addDrawableJudgement(DrawableHitObject drawableHitObject)
{
var judgement = new DrawableOsuJudgement();
judgement.Apply(new JudgementResult(drawableHitObject.HitObject, new OsuJudgement())
{
Type = HitResult.Great,
TimeOffset = Time.Current
}, drawableHitObject);
judgementContainer.Add(judgement);
}
private HitCircle createHitCircle()
{
var circle = new HitCircle();
circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
return circle;
}
}
}

View File

@ -0,0 +1,315 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osu.Game.Storyboards;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestScenePauseInputHandling : PlayerTestScene
{
private Ruleset currentRuleset = new OsuRuleset();
protected override Ruleset CreatePlayerRuleset() => currentRuleset;
protected override bool HasCustomSteps => true;
[Resolved]
private AudioManager audioManager { get; set; } = null!;
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
{
HitObjects =
{
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 0,
},
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 5000,
}
}
};
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
[SetUp]
public void SetUp() => Schedule(() =>
{
foreach (var key in InputManager.CurrentState.Keyboard.Keys)
InputManager.ReleaseKey(key);
InputManager.MoveMouseTo(Content);
LocalConfig.SetValue(OsuSetting.KeyOverlay, true);
});
[Test]
public void TestOsuInputNotReceivedWhilePaused()
{
KeyCounter counter = null!;
loadPlayer(() => new OsuRuleset());
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<OsuAction> actionTrigger && actionTrigger.Action == OsuAction.LeftButton));
checkKey(() => counter, 0, false);
AddStep("press Z", () => InputManager.PressKey(Key.Z));
checkKey(() => counter, 1, true);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counter, 1, false);
AddStep("pause", () => Player.Pause());
AddStep("press Z", () => InputManager.PressKey(Key.Z));
checkKey(() => counter, 1, false);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counter, 1, false);
AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));
checkKey(() => counter, 2, true);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counter, 2, false);
AddStep("press Z", () => InputManager.PressKey(Key.Z));
checkKey(() => counter, 3, true);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counter, 3, false);
}
[Test]
public void TestManiaInputNotReceivedWhilePaused()
{
KeyCounter counter = null!;
loadPlayer(() => new ManiaRuleset());
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Special1));
checkKey(() => counter, 0, false);
AddStep("press space", () => InputManager.PressKey(Key.Space));
checkKey(() => counter, 1, true);
AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 1, false);
AddStep("pause", () => Player.Pause());
AddStep("press space", () => InputManager.PressKey(Key.Space));
checkKey(() => counter, 1, false);
AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 1, false);
AddStep("resume", () => Player.Resume());
AddUntilStep("wait for resume", () => Player.GameplayClockContainer.IsRunning);
AddStep("press space", () => InputManager.PressKey(Key.Space));
checkKey(() => counter, 2, true);
AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 2, false);
}
[Test]
public void TestOsuPreviouslyHeldInputReleaseOnResume()
{
KeyCounter counterZ = null!;
KeyCounter counterX = null!;
loadPlayer(() => new OsuRuleset());
AddStep("get key counter Z", () => counterZ = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<OsuAction> actionTrigger && actionTrigger.Action == OsuAction.LeftButton));
AddStep("get key counter X", () => counterX = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<OsuAction> actionTrigger && actionTrigger.Action == OsuAction.RightButton));
AddStep("press Z", () => InputManager.PressKey(Key.Z));
AddStep("pause", () => Player.Pause());
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press and release Z", () => InputManager.Key(Key.Z));
checkKey(() => counterZ, 1, false);
AddStep("press X", () => InputManager.PressKey(Key.X));
AddStep("pause", () => Player.Pause());
AddStep("release X", () => InputManager.ReleaseKey(Key.X));
checkKey(() => counterX, 1, true);
AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));
checkKey(() => counterZ, 2, true);
checkKey(() => counterX, 1, false);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counterZ, 2, false);
}
[Test]
public void TestManiaPreviouslyHeldInputReleaseOnResume()
{
KeyCounter counter = null!;
loadPlayer(() => new ManiaRuleset());
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Special1));
AddStep("press space", () => InputManager.PressKey(Key.Space));
AddStep("pause", () => Player.Pause());
AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 1, true);
AddStep("resume", () => Player.Resume());
AddUntilStep("wait for resume", () => Player.GameplayClockContainer.IsRunning);
checkKey(() => counter, 1, false);
}
[Test]
public void TestOsuHeldInputRemainHeldAfterResume()
{
KeyCounter counterZ = null!;
KeyCounter counterX = null!;
loadPlayer(() => new OsuRuleset());
AddStep("get key counter Z", () => counterZ = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<OsuAction> actionTrigger && actionTrigger.Action == OsuAction.LeftButton));
AddStep("get key counter X", () => counterX = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<OsuAction> actionTrigger && actionTrigger.Action == OsuAction.RightButton));
AddStep("press Z", () => InputManager.PressKey(Key.Z));
AddStep("pause", () => Player.Pause());
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));
checkKey(() => counterZ, 1, true);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counterZ, 1, false);
AddStep("press X", () => InputManager.PressKey(Key.X));
checkKey(() => counterX, 1, true);
AddStep("pause", () => Player.Pause());
AddStep("release X", () => InputManager.ReleaseKey(Key.X));
AddStep("press X", () => InputManager.PressKey(Key.X));
AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));
checkKey(() => counterZ, 2, true);
checkKey(() => counterX, 1, true);
AddStep("release X", () => InputManager.ReleaseKey(Key.X));
checkKey(() => counterX, 1, false);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counterZ, 2, false);
}
[Test]
public void TestManiaHeldInputRemainHeldAfterResume()
{
KeyCounter counter = null!;
loadPlayer(() => new ManiaRuleset());
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Special1));
AddStep("press space", () => InputManager.PressKey(Key.Space));
checkKey(() => counter, 1, true);
AddStep("pause", () => Player.Pause());
AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
AddStep("press space", () => InputManager.PressKey(Key.Space));
AddStep("resume", () => Player.Resume());
AddUntilStep("wait for resume", () => Player.GameplayClockContainer.IsRunning);
checkKey(() => counter, 1, true);
AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 1, false);
}
[Test]
public void TestOsuRegisterInputFromPressingOrangeCursorButPressIsBlocked()
{
KeyCounter counter = null!;
loadPlayer(() => new OsuRuleset());
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<OsuAction> actionTrigger && actionTrigger.Action == OsuAction.LeftButton));
AddStep("pause", () => Player.Pause());
AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));
// ensure the input manager receives the Z button press...
checkKey(() => counter, 1, true);
AddAssert("button is pressed in kbc", () => Player.DrawableRuleset.Playfield.FindClosestParent<OsuInputManager>()!.PressedActions.Single() == OsuAction.LeftButton);
// ...but also ensure the hit circle in front of the cursor isn't hit by checking max combo.
AddAssert("circle not hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(0));
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counter, 1, false);
AddAssert("button is released in kbc", () => !Player.DrawableRuleset.Playfield.FindClosestParent<OsuInputManager>()!.PressedActions.Any());
}
private void loadPlayer(Func<Ruleset> createRuleset)
{
AddStep("set ruleset", () => currentRuleset = createRuleset());
AddStep("load player", LoadPlayer);
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
AddUntilStep("wait for hud", () => Player.HUDOverlay.ChildrenOfType<SkinComponentsContainer>().All(s => s.ComponentsLoaded));
AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(0));
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(0).Within(500));
AddAssert("not in break", () => !Player.IsBreakTime.Value);
AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield));
}
private void checkKey(Func<KeyCounter> counter, int count, bool active)
{
AddAssert($"key count = {count}", () => counter().CountPresses.Value, () => Is.EqualTo(count));
AddAssert($"key active = {active}", () => counter().IsActive.Value, () => Is.EqualTo(active));
}
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new PausePlayer();
private partial class PausePlayer : TestPlayer
{
protected override double PauseCooldownDuration => 0;
public PausePlayer()
: base(allowPause: true, showResults: false)
{
}
}
}
}

View File

@ -117,6 +117,9 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("state entered downloading", () => downloadStarted); AddAssert("state entered downloading", () => downloadStarted);
AddUntilStep("state left downloading", () => downloadFinished); AddUntilStep("state left downloading", () => downloadFinished);
AddStep("change score to null", () => downloadButton.Score.Value = null);
AddUntilStep("state changed to unknown", () => downloadButton.State.Value, () => Is.EqualTo(DownloadState.Unknown));
} }
[Test] [Test]

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
@ -34,7 +32,7 @@ namespace osu.Game.Tests.Visual.Menus
[Test] [Test]
public void TestMusicNavigationActions() public void TestMusicNavigationActions()
{ {
Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null; Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null!;
// ensure we have at least two beatmaps available to identify the direction the music controller navigated to. // ensure we have at least two beatmaps available to identify the direction the music controller navigated to.
AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5); AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5);
@ -62,14 +60,22 @@ namespace osu.Game.Tests.Visual.Menus
AddUntilStep("track restarted", () => Game.MusicController.CurrentTrack.CurrentTime < 5000); AddUntilStep("track restarted", () => Game.MusicController.CurrentTrack.CurrentTime < 5000);
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev)); AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
AddAssert("track changed to previous", () => AddUntilStep("track changed to previous", () =>
trackChangeQueue.Count == 1 && trackChangeQueue.Count == 1 &&
trackChangeQueue.Dequeue().changeDirection == TrackChangeDirection.Prev); trackChangeQueue.Dequeue().changeDirection == TrackChangeDirection.Prev);
AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext)); AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext));
AddAssert("track changed to next", () => AddUntilStep("track changed to next", () =>
trackChangeQueue.Count == 1 && trackChangeQueue.Count == 1 &&
trackChangeQueue.Dequeue().changeDirection == TrackChangeDirection.Next); trackChangeQueue.Peek().changeDirection == TrackChangeDirection.Next);
AddUntilStep("wait until track switches", () => trackChangeQueue.Count == 2);
AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext));
AddUntilStep("track changed to next", () =>
trackChangeQueue.Count == 3 &&
trackChangeQueue.Peek().changeDirection == TrackChangeDirection.Next);
AddAssert("track actually changed", () => !trackChangeQueue.First().working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo));
} }
} }
} }

View File

@ -12,6 +12,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -319,16 +320,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
}); });
AddUntilStep("wait for load", () => playlist.ChildrenOfType<DrawableLinkCompiler>().Any() && playlist.ChildrenOfType<BeatmapCardThumbnail>().First().DrawWidth > 0); AddUntilStep("wait for load", () => playlist.ChildrenOfType<DrawableLinkCompiler>().Any() && playlist.ChildrenOfType<BeatmapCardThumbnail>().First().DrawWidth > 0);
AddStep("move mouse to first item title", () =>
{ AddStep("move mouse to first item title", () => InputManager.MoveMouseTo(playlist.ChildrenOfType<LinkFlowContainer>().First().ChildrenOfType<SpriteText>().First()));
var drawQuad = playlist.ChildrenOfType<LinkFlowContainer>().First().ScreenSpaceDrawQuad;
var location = (drawQuad.TopLeft + drawQuad.BottomLeft) / 2 + new Vector2(drawQuad.Width * 0.2f, 0);
InputManager.MoveMouseTo(location);
});
AddAssert("first item title not hovered", () => playlist.ChildrenOfType<DrawableLinkCompiler>().First().IsHovered, () => Is.False); AddAssert("first item title not hovered", () => playlist.ChildrenOfType<DrawableLinkCompiler>().First().IsHovered, () => Is.False);
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
AddStep("click title", () =>
{
InputManager.MoveMouseTo(playlist.ChildrenOfType<LinkFlowContainer>().First().ChildrenOfType<SpriteText>().First());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("first item selected", () => playlist.ChildrenOfType<DrawableRoomPlaylistItem>().First().IsSelectedItem, () => Is.True); AddUntilStep("first item selected", () => playlist.ChildrenOfType<DrawableRoomPlaylistItem>().First().IsSelectedItem, () => Is.True);
// implies being clickable.
AddUntilStep("first item title hovered", () => playlist.ChildrenOfType<DrawableLinkCompiler>().First().IsHovered, () => Is.True); AddUntilStep("first item title hovered", () => playlist.ChildrenOfType<DrawableLinkCompiler>().First().IsHovered, () => Is.True);
AddStep("move mouse to second item results button", () => InputManager.MoveMouseTo(playlist.ChildrenOfType<GrayButton>().ElementAt(5))); AddStep("move mouse to second item results button", () => InputManager.MoveMouseTo(playlist.ChildrenOfType<GrayButton>().ElementAt(5)));

View File

@ -3,6 +3,7 @@
#nullable disable #nullable disable
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
@ -16,6 +17,7 @@ using osu.Framework.Testing;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Footer;
using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay;
using osu.Game.Utils; using osu.Game.Utils;
using osuTK.Input; using osuTK.Input;
@ -26,6 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
private FreeModSelectOverlay freeModSelectOverlay; private FreeModSelectOverlay freeModSelectOverlay;
private FooterButtonFreeMods footerButtonFreeMods; private FooterButtonFreeMods footerButtonFreeMods;
private ScreenFooter footer;
private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>(); private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -127,7 +130,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
createFreeModSelect(); createFreeModSelect();
AddAssert("overlay select all button enabled", () => freeModSelectOverlay.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value); AddAssert("overlay select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
AddAssert("footer button displays off", () => footerButtonFreeMods.ChildrenOfType<IHasText>().Any(t => t.Text == "off")); AddAssert("footer button displays off", () => footerButtonFreeMods.ChildrenOfType<IHasText>().Any(t => t.Text == "off"));
AddStep("click footer select all button", () => AddStep("click footer select all button", () =>
@ -150,7 +153,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void createFreeModSelect() private void createFreeModSelect()
{ {
AddStep("create free mod select screen", () => Children = new Drawable[] AddStep("create free mod select screen", () => Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{ {
freeModSelectOverlay = new FreeModSelectOverlay freeModSelectOverlay = new FreeModSelectOverlay
{ {
@ -160,9 +166,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Y = -ScreenFooter.HEIGHT,
Current = { BindTarget = freeModSelectOverlay.SelectedMods }, Current = { BindTarget = freeModSelectOverlay.SelectedMods },
}, },
footer = new ScreenFooter(),
},
CachedDependencies = new (Type, object)[] { (typeof(ScreenFooter), footer) },
}); });
AddUntilStep("all column content loaded", AddUntilStep("all column content loaded",
() => freeModSelectOverlay.ChildrenOfType<ModColumn>().Any() () => freeModSelectOverlay.ChildrenOfType<ModColumn>().Any()
&& freeModSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded)); && freeModSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded));

View File

@ -312,14 +312,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for join", () => RoomJoined); AddUntilStep("wait for join", () => RoomJoined);
ClickButtonWhenEnabled<UserModSelectButton>(); ClickButtonWhenEnabled<UserModSelectButton>();
AddAssert("mod select shows unranked", () => screen.UserModsSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().Ranked.Value == false); AddAssert("mod select shows unranked", () => this.ChildrenOfType<RankingInformationDisplay>().Single().Ranked.Value == false);
AddAssert("score multiplier = 1.20", () => screen.UserModsSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); AddAssert("score multiplier = 1.20", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01));
AddStep("select flashlight", () => screen.UserModsSelectOverlay.ChildrenOfType<ModPanel>().Single(m => m.Mod is ModFlashlight).TriggerClick()); AddStep("select flashlight", () => screen.UserModsSelectOverlay.ChildrenOfType<ModPanel>().Single(m => m.Mod is ModFlashlight).TriggerClick());
AddAssert("score multiplier = 1.35", () => screen.UserModsSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.35).Within(0.01)); AddAssert("score multiplier = 1.35", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.35).Within(0.01));
AddStep("change flashlight setting", () => ((OsuModFlashlight)screen.UserModsSelectOverlay.SelectedMods.Value.Single()).FollowDelay.Value = 1200); AddStep("change flashlight setting", () => ((OsuModFlashlight)screen.UserModsSelectOverlay.SelectedMods.Value.Single()).FollowDelay.Value = 1200);
AddAssert("score multiplier = 1.20", () => screen.UserModsSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); AddAssert("score multiplier = 1.20", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01));
} }
private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen

View File

@ -1,12 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Screens; using osu.Framework.Screens;
@ -204,12 +208,16 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("Set current beatmap to default", () => Game.Beatmap.SetDefault()); AddStep("Set current beatmap to default", () => Game.Beatmap.SetDefault());
AddStep("Push editor loader", () => Game.ScreenStack.Push(new EditorLoader())); DelayedLoadEditorLoader loader = null!;
AddStep("Push editor loader", () => Game.ScreenStack.Push(loader = new DelayedLoadEditorLoader()));
AddUntilStep("Wait for loader current", () => Game.ScreenStack.CurrentScreen is EditorLoader); AddUntilStep("Wait for loader current", () => Game.ScreenStack.CurrentScreen is EditorLoader);
AddUntilStep("wait for editor load start", () => loader.Editor != null);
AddStep("Close editor while loading", () => Game.ScreenStack.CurrentScreen.Exit()); AddStep("Close editor while loading", () => Game.ScreenStack.CurrentScreen.Exit());
AddStep("allow editor load", () => loader.AllowLoad.Set());
AddUntilStep("wait for editor ready", () => loader.Editor!.LoadState >= LoadState.Ready);
AddUntilStep("Wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu); AddUntilStep("Wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
AddAssert("Check no new beatmaps were made", () => allBeatmapSets().SequenceEqual(beatmapSets)); AddAssert("Check no new beatmaps were made", allBeatmapSets, () => Is.EquivalentTo(beatmapSets));
BeatmapSetInfo[] allBeatmapSets() => Game.Realm.Run(realm => realm.All<BeatmapSetInfo>().Where(x => !x.DeletePending).ToArray()); BeatmapSetInfo[] allBeatmapSets() => Game.Realm.Run(realm => realm.All<BeatmapSetInfo>().Where(x => !x.DeletePending).ToArray());
} }
@ -356,5 +364,33 @@ namespace osu.Game.Tests.Visual.Navigation
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;
private partial class DelayedLoadEditorLoader : EditorLoader
{
public readonly ManualResetEventSlim AllowLoad = new ManualResetEventSlim();
public Editor? Editor { get; private set; }
protected override Editor CreateEditor() => Editor = new DelayedLoadEditor(this);
}
private partial class DelayedLoadEditor : Editor
{
private readonly DelayedLoadEditorLoader loader;
public DelayedLoadEditor(DelayedLoadEditorLoader loader)
: base(loader)
{
this.loader = loader;
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
// Importantly, this occurs before base.load().
if (!loader.AllowLoad.Wait(TimeSpan.FromSeconds(10)))
throw new TimeoutException();
return base.CreateChildDependencies(parent);
}
}
} }
} }

View File

@ -838,18 +838,25 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestExitWithOperationInProgress() public void TestExitWithOperationInProgress()
{ {
AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault() != null); int x = 0;
AddUntilStep("wait for dialog overlay", () =>
{
x = 0;
return Game.ChildrenOfType<DialogOverlay>().SingleOrDefault() != null;
});
AddRepeatStep("start ongoing operation", () => AddRepeatStep("start ongoing operation", () =>
{ {
Game.Notifications.Post(new ProgressNotification Game.Notifications.Post(new ProgressNotification
{ {
Text = "Something is still running", Text = $"Something is still running #{++x}",
Progress = 0.5f, Progress = 0.5f,
State = ProgressNotificationState.Active, State = ProgressNotificationState.Active,
}); });
}, 15); }, 15);
AddAssert("all notifications = 15", () => Game.Notifications.AllNotifications.Count(), () => Is.EqualTo(15));
AddStep("Hold escape", () => InputManager.PressKey(Key.Escape)); AddStep("Hold escape", () => InputManager.PressKey(Key.Escape));
AddUntilStep("confirmation dialog shown", () => Game.ChildrenOfType<DialogOverlay>().Single().CurrentDialog is ConfirmExitDialog); AddUntilStep("confirmation dialog shown", () => Game.ChildrenOfType<DialogOverlay>().Single().CurrentDialog is ConfirmExitDialog);
AddStep("Release escape", () => InputManager.ReleaseKey(Key.Escape)); AddStep("Release escape", () => InputManager.ReleaseKey(Key.Escape));
@ -945,6 +952,8 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestTouchScreenDetectionAtSongSelect() public void TestTouchScreenDetectionAtSongSelect()
{ {
AddUntilStep("wait for settings", () => Game.Settings.IsLoaded);
AddStep("touch logo", () => AddStep("touch logo", () =>
{ {
var button = Game.ChildrenOfType<OsuLogo>().Single(); var button = Game.ChildrenOfType<OsuLogo>().Single();

View File

@ -157,6 +157,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
setUpCommentsResponse(getExampleComments()); setUpCommentsResponse(getExampleComments());
AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123)); AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123));
AddUntilStep("comments shown", () => commentsContainer.ChildrenOfType<DrawableComment>().Any());
setUpPostResponse(); setUpPostResponse();
AddStep("enter text", () => editorTextBox.Current.Value = "comm"); AddStep("enter text", () => editorTextBox.Current.Value = "comm");
@ -175,6 +176,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
setUpCommentsResponse(getExampleComments()); setUpCommentsResponse(getExampleComments());
AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123)); AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123));
AddUntilStep("comments shown", () => commentsContainer.ChildrenOfType<DrawableComment>().Any());
setUpPostResponse(true); setUpPostResponse(true);
AddStep("enter text", () => editorTextBox.Current.Value = "comm"); AddStep("enter text", () => editorTextBox.Current.Value = "comm");

View File

@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat; using osu.Game.Online.Chat;
using osu.Game.Overlays.Chat; using osu.Game.Overlays.Chat;
@ -40,6 +41,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 3, Id = 3,
Username = "LocalUser" Username = "LocalUser"
}; };
string uuid = Guid.NewGuid().ToString(); string uuid = Guid.NewGuid().ToString();
AddStep("add local echo message", () => channel.AddLocalEcho(new LocalEchoMessage AddStep("add local echo message", () => channel.AddLocalEcho(new LocalEchoMessage
{ {
@ -83,5 +85,38 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("three day separators present", () => drawableChannel.ChildrenOfType<DaySeparator>().Count() == 3); AddUntilStep("three day separators present", () => drawableChannel.ChildrenOfType<DaySeparator>().Count() == 3);
AddAssert("last day separator is from correct day", () => drawableChannel.ChildrenOfType<DaySeparator>().Last().Date.Date == new DateTime(2022, 11, 22)); AddAssert("last day separator is from correct day", () => drawableChannel.ChildrenOfType<DaySeparator>().Last().Date.Date == new DateTime(2022, 11, 22));
} }
[Test]
public void TestBackgroundAlternating()
{
int messageCount = 1;
AddRepeatStep("add messages", () =>
{
channel.AddNewMessages(new Message(messageCount)
{
Sender = new APIUser
{
Id = 3,
Username = "LocalUser " + RNG.Next(0, int.MaxValue - 100).ToString("N")
},
Content = "Hi there all!",
Timestamp = new DateTimeOffset(2022, 11, 21, 20, messageCount, 13, TimeSpan.Zero),
Uuid = Guid.NewGuid().ToString(),
});
messageCount++;
}, 10);
AddUntilStep("10 message present", () => drawableChannel.ChildrenOfType<ChatLine>().Count() == 10);
int checkCount = 0;
AddRepeatStep("check background", () =>
{
// +1 because the day separator take one index
Assert.AreEqual((checkCount + 1) % 2 == 0, drawableChannel.ChildrenOfType<ChatLine>().ToList()[checkCount].AlternatingBackground);
checkCount++;
}, 10);
}
} }
} }

View File

@ -0,0 +1,64 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Profile;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Rulesets.Osu;
using osuTK;
namespace osu.Game.Tests.Visual.Online
{
public partial class TestSceneUserProfileDailyChallenge : OsuManualInputManagerTestScene
{
[Cached]
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>(new UserProfileData(new APIUser(), new OsuRuleset().RulesetInfo));
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
protected override void LoadComplete()
{
base.LoadComplete();
DailyChallengeStatsDisplay display = null!;
AddSliderStep("daily", 0, 999, 2, v => update(s => s.DailyStreakCurrent = v));
AddSliderStep("daily best", 0, 999, 2, v => update(s => s.DailyStreakBest = v));
AddSliderStep("weekly", 0, 250, 1, v => update(s => s.WeeklyStreakCurrent = v));
AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v));
AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v));
AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v));
AddSliderStep("playcount", 0, 999, 0, v => update(s => s.PlayCount = v));
AddStep("create", () =>
{
Clear();
Add(new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background2,
});
Add(display = new DailyChallengeStatsDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(1f),
User = { BindTarget = User },
});
});
AddStep("hover", () => InputManager.MoveMouseTo(display));
}
private void update(Action<APIUserDailyChallengeStatistics> change)
{
change.Invoke(User.Value!.User.DailyChallengeStatistics);
User.Value = new UserProfileData(User.Value.User, User.Value.Ruleset);
}
}
}

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Online.API; using osu.Game.Online.API;
@ -24,7 +25,17 @@ namespace osu.Game.Tests.Visual.Online
[SetUpSteps] [SetUpSteps]
public void SetUp() public void SetUp()
{ {
AddStep("create profile overlay", () => Child = profile = new UserProfileOverlay()); AddStep("create profile overlay", () =>
{
profile = new UserProfileOverlay();
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[] { (typeof(UserProfileOverlay), profile) },
Child = profile,
};
});
} }
[Test] [Test]
@ -111,6 +122,90 @@ namespace osu.Game.Tests.Visual.Online
AddStep("complete request", () => pendingRequest.TriggerSuccess(TEST_USER)); AddStep("complete request", () => pendingRequest.TriggerSuccess(TEST_USER));
} }
[Test]
public void TestCustomColourScheme()
{
int hue = 0;
AddSliderStep("hue", 0, 360, 222, h => hue = h);
AddStep("set up request handling", () =>
{
dummyAPI.HandleRequest = req =>
{
if (req is GetUserRequest getUserRequest)
{
getUserRequest.TriggerSuccess(new APIUser
{
Username = $"Colorful #{hue}",
Id = 1,
CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
ProfileHue = hue,
PlayMode = "osu",
});
return true;
}
return false;
};
});
AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 }));
}
[Test]
public void TestCustomColourSchemeWithReload()
{
int hue = 0;
GetUserRequest pendingRequest = null!;
AddSliderStep("hue", 0, 360, 222, h => hue = h);
AddStep("set up request handling", () =>
{
dummyAPI.HandleRequest = req =>
{
if (req is GetUserRequest getUserRequest)
{
pendingRequest = getUserRequest;
return true;
}
return false;
};
});
AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 }));
AddWaitStep("wait some", 3);
AddStep("complete request", () => pendingRequest.TriggerSuccess(new APIUser
{
Username = $"Colorful #{hue}",
Id = 1,
CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
ProfileHue = hue,
PlayMode = "osu",
}));
int hue2 = 0;
AddSliderStep("hue 2", 0, 360, 50, h => hue2 = h);
AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 }));
AddWaitStep("wait some", 3);
AddStep("complete request", () => pendingRequest.TriggerSuccess(new APIUser
{
Username = $"Colorful #{hue2}",
Id = 1,
CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
ProfileHue = hue2,
PlayMode = "osu",
}));
}
public static readonly APIUser TEST_USER = new APIUser public static readonly APIUser TEST_USER = new APIUser
{ {
Username = @"Somebody", Username = @"Somebody",
@ -201,6 +296,15 @@ namespace osu.Game.Tests.Visual.Online
ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor.png", ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor.png",
}, },
}, },
DailyChallengeStatistics = new APIUserDailyChallengeStatistics
{
DailyStreakCurrent = 231,
WeeklyStreakCurrent = 18,
DailyStreakBest = 370,
WeeklyStreakBest = 51,
Top10PercentPlacements = 345,
Top50PercentPlacements = 427,
},
Title = "osu!volunteer", Title = "osu!volunteer",
Colour = "ff0000", Colour = "ff0000",
Achievements = Array.Empty<APIUserAchievement>(), Achievements = Array.Empty<APIUserAchievement>(),

View File

@ -135,7 +135,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() >= beforePanelCount + scores_per_result); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
} }
} }
@ -156,7 +156,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() >= beforePanelCount + scores_per_result); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count()); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
@ -191,7 +191,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible); AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() >= beforePanelCount + scores_per_result); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden); AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden);
} }
} }

View File

@ -181,12 +181,6 @@ namespace osu.Game.Tests.Visual.SongSelect
#endregion #endregion
protected override void Update()
{
base.Update();
Stack.Padding = new MarginPadding { Bottom = screenScreenFooter.DrawHeight - screenScreenFooter.Y };
}
private void updateFooter(IScreen? _, IScreen? newScreen) private void updateFooter(IScreen? _, IScreen? newScreen)
{ {
if (newScreen is IOsuScreen osuScreen && osuScreen.ShowFooter) if (newScreen is IOsuScreen osuScreen && osuScreen.ShowFooter)

View File

@ -11,6 +11,8 @@ using Moq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -20,6 +22,7 @@ using osu.Game.Overlays;
using osu.Game.Overlays.FirstRunSetup; using osu.Game.Overlays.FirstRunSetup;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Screens; using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -28,6 +31,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public partial class TestSceneFirstRunSetupOverlay : OsuManualInputManagerTestScene public partial class TestSceneFirstRunSetupOverlay : OsuManualInputManagerTestScene
{ {
private FirstRunSetupOverlay overlay; private FirstRunSetupOverlay overlay;
private ScreenFooter footer;
private readonly Mock<TestPerformerFromScreenRunner> performer = new Mock<TestPerformerFromScreenRunner>(); private readonly Mock<TestPerformerFromScreenRunner> performer = new Mock<TestPerformerFromScreenRunner>();
@ -60,19 +64,16 @@ namespace osu.Game.Tests.Visual.UserInterface
.Callback((Notification n) => lastNotification = n); .Callback((Notification n) => lastNotification = n);
}); });
AddStep("add overlay", () => createOverlay();
{
Child = overlay = new FirstRunSetupOverlay AddStep("show overlay", () => overlay.Show());
{
State = { Value = Visibility.Visible }
};
});
} }
[Test] [Test]
public void TestBasic() public void TestBasic()
{ {
AddAssert("overlay visible", () => overlay.State.Value == Visibility.Visible); AddAssert("overlay visible", () => overlay.State.Value == Visibility.Visible);
AddAssert("footer visible", () => footer.State.Value == Visibility.Visible);
} }
[Test] [Test]
@ -82,16 +83,13 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("step through", () => AddUntilStep("step through", () =>
{ {
if (overlay.CurrentScreen?.IsLoaded != false) overlay.NextButton.TriggerClick(); if (overlay.CurrentScreen?.IsLoaded != false) overlay.NextButton.AsNonNull().TriggerClick();
return overlay.State.Value == Visibility.Hidden; return overlay.State.Value == Visibility.Hidden;
}); });
AddAssert("first run false", () => !LocalConfig.Get<bool>(OsuSetting.ShowFirstRunSetup)); AddAssert("first run false", () => !LocalConfig.Get<bool>(OsuSetting.ShowFirstRunSetup));
AddStep("add overlay", () => createOverlay();
{
Child = overlay = new FirstRunSetupOverlay();
});
AddWaitStep("wait some", 5); AddWaitStep("wait some", 5);
@ -109,7 +107,7 @@ namespace osu.Game.Tests.Visual.UserInterface
if (keyboard) if (keyboard)
InputManager.Key(Key.Enter); InputManager.Key(Key.Enter);
else else
overlay.NextButton.TriggerClick(); overlay.NextButton.AsNonNull().TriggerClick();
} }
return overlay.State.Value == Visibility.Hidden; return overlay.State.Value == Visibility.Hidden;
@ -128,11 +126,9 @@ namespace osu.Game.Tests.Visual.UserInterface
[TestCase(true)] [TestCase(true)]
public void TestBackButton(bool keyboard) public void TestBackButton(bool keyboard)
{ {
AddAssert("back button disabled", () => !overlay.BackButton.Enabled.Value);
AddUntilStep("step to last", () => AddUntilStep("step to last", () =>
{ {
var nextButton = overlay.NextButton; var nextButton = overlay.NextButton.AsNonNull();
if (overlay.CurrentScreen?.IsLoaded != false) if (overlay.CurrentScreen?.IsLoaded != false)
nextButton.TriggerClick(); nextButton.TriggerClick();
@ -142,24 +138,29 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("step back to start", () => AddUntilStep("step back to start", () =>
{ {
if (overlay.CurrentScreen?.IsLoaded != false) if (overlay.CurrentScreen?.IsLoaded != false && !(overlay.CurrentScreen is ScreenWelcome))
{ {
if (keyboard) if (keyboard)
InputManager.Key(Key.Escape); InputManager.Key(Key.Escape);
else else
overlay.BackButton.TriggerClick(); footer.BackButton.TriggerClick();
} }
return overlay.CurrentScreen is ScreenWelcome; return overlay.CurrentScreen is ScreenWelcome;
}); });
AddAssert("back button disabled", () => !overlay.BackButton.Enabled.Value); AddAssert("overlay not dismissed", () => overlay.State.Value == Visibility.Visible);
if (keyboard) if (keyboard)
{ {
AddStep("exit via keyboard", () => InputManager.Key(Key.Escape)); AddStep("exit via keyboard", () => InputManager.Key(Key.Escape));
AddAssert("overlay dismissed", () => overlay.State.Value == Visibility.Hidden); AddAssert("overlay dismissed", () => overlay.State.Value == Visibility.Hidden);
} }
else
{
AddStep("press back button", () => footer.BackButton.TriggerClick());
AddAssert("overlay dismissed", () => overlay.State.Value == Visibility.Hidden);
}
} }
[Test] [Test]
@ -185,7 +186,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test] [Test]
public void TestResumeViaNotification() public void TestResumeViaNotification()
{ {
AddStep("step to next", () => overlay.NextButton.TriggerClick()); AddStep("step to next", () => overlay.NextButton.AsNonNull().TriggerClick());
AddAssert("is at known screen", () => overlay.CurrentScreen is ScreenUIScale); AddAssert("is at known screen", () => overlay.CurrentScreen is ScreenUIScale);
@ -200,6 +201,27 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("is resumed", () => overlay.CurrentScreen is ScreenUIScale); AddAssert("is resumed", () => overlay.CurrentScreen is ScreenUIScale);
} }
private void createOverlay()
{
AddStep("add overlay", () =>
{
var receptor = new ScreenFooter.BackReceptor();
footer = new ScreenFooter(receptor);
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) },
Children = new Drawable[]
{
receptor,
overlay = new FirstRunSetupOverlay(),
footer,
}
};
});
}
// interface mocks break hot reload, mocking this stub implementation instead works around it. // interface mocks break hot reload, mocking this stub implementation instead works around it.
// see: https://github.com/moq/moq4/issues/1252 // see: https://github.com/moq/moq4/issues/1252
[UsedImplicitly] [UsedImplicitly]

View File

@ -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.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -10,6 +11,7 @@ using osu.Game.Localisation;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Metadata; using osu.Game.Online.Metadata;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osuTK.Input; using osuTK.Input;
using Color4 = osuTK.Graphics.Color4; using Color4 = osuTK.Graphics.Color4;
@ -39,8 +41,6 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test] [Test]
public void TestDailyChallengeButton() public void TestDailyChallengeButton()
{ {
AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null));
AddStep("set up API", () => dummyAPI.HandleRequest = req => AddStep("set up API", () => dummyAPI.HandleRequest = req =>
{ {
switch (req) switch (req)
@ -67,17 +67,45 @@ namespace osu.Game.Tests.Visual.UserInterface
} }
}); });
AddStep("add button", () => Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D) NotificationOverlay notificationOverlay = null!;
{ DependencyProvidingContainer buttonContainer = null!;
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
ButtonSystemState = ButtonSystemState.TopLevel,
});
AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo
{ {
RoomID = 1234, RoomID = 1234,
})); }));
AddStep("add content", () =>
{
notificationOverlay = new NotificationOverlay();
Children = new Drawable[]
{
notificationOverlay,
buttonContainer = new DependencyProvidingContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)],
Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
ButtonSystemState = ButtonSystemState.TopLevel,
},
},
};
});
AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero);
AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null));
AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero);
AddStep("hide button's parent", () => buttonContainer.Hide());
AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo
{
RoomID = 1234,
}));
AddAssert("notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1));
} }
} }
} }

View File

@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.UserInterface
} }
[Test] [Test]
public void TestOutOfRangeValueStillApplied() public void TestValueAboveRangeStillApplied()
{ {
AddStep("set override cs to 11", () => modDifficultyAdjust.CircleSize.Value = 11); AddStep("set override cs to 11", () => modDifficultyAdjust.CircleSize.Value = 11);
@ -91,6 +91,28 @@ namespace osu.Game.Tests.Visual.UserInterface
checkBindableAtValue("Circle Size", 11); checkBindableAtValue("Circle Size", 11);
} }
[Test]
public void TestValueBelowRangeStillApplied()
{
AddStep("set override cs to -5", () => modDifficultyAdjust.ApproachRate.Value = -5);
checkSliderAtValue("Approach Rate", -5);
checkBindableAtValue("Approach Rate", -5);
// this is a no-op, just showing that it won't reset the value during deserialisation.
setExtendedLimits(false);
checkSliderAtValue("Approach Rate", -5);
checkBindableAtValue("Approach Rate", -5);
// setting extended limits will reset the serialisation exception.
// this should be fine as the goal is to allow, at most, the value of extended limits.
setExtendedLimits(true);
checkSliderAtValue("Approach Rate", -5);
checkBindableAtValue("Approach Rate", -5);
}
[Test] [Test]
public void TestExtendedLimits() public void TestExtendedLimits()
{ {
@ -109,6 +131,11 @@ namespace osu.Game.Tests.Visual.UserInterface
checkSliderAtValue("Circle Size", 11); checkSliderAtValue("Circle Size", 11);
checkBindableAtValue("Circle Size", 11); checkBindableAtValue("Circle Size", 11);
setSliderValue("Approach Rate", -5);
checkSliderAtValue("Approach Rate", -5);
checkBindableAtValue("Approach Rate", -5);
setExtendedLimits(false); setExtendedLimits(false);
checkSliderAtValue("Circle Size", 10); checkSliderAtValue("Circle Size", 10);

View File

@ -24,6 +24,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Screens.Footer;
using osu.Game.Tests.Mods; using osu.Game.Tests.Mods;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -94,12 +95,28 @@ namespace osu.Game.Tests.Visual.UserInterface
private void createScreen() private void createScreen()
{ {
AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay AddStep("create screen", () =>
{
var receptor = new ScreenFooter.BackReceptor();
var footer = new ScreenFooter(receptor);
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) },
Children = new Drawable[]
{
receptor,
modSelectOverlay = new TestModSelectOverlay
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible }, State = { Value = Visibility.Visible },
Beatmap = Beatmap.Value, Beatmap = { Value = Beatmap.Value },
SelectedMods = { BindTarget = SelectedMods } SelectedMods = { BindTarget = SelectedMods },
},
footer,
}
};
}); });
waitForColumnLoad(); waitForColumnLoad();
} }
@ -120,7 +137,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("mod multiplier correct", () => AddAssert("mod multiplier correct", () =>
{ {
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier); double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value); return Precision.AlmostEquals(multiplier, this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
}); });
assertCustomisationToggleState(disabled: false, active: false); assertCustomisationToggleState(disabled: false, active: false);
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any()); AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
@ -135,7 +152,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("mod multiplier correct", () => AddAssert("mod multiplier correct", () =>
{ {
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier); double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value); return Precision.AlmostEquals(multiplier, this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
}); });
assertCustomisationToggleState(disabled: false, active: false); assertCustomisationToggleState(disabled: false, active: false);
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any()); AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
@ -757,7 +774,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("click back button", () => AddStep("click back button", () =>
{ {
InputManager.MoveMouseTo(modSelectOverlay.BackButton); InputManager.MoveMouseTo(this.ChildrenOfType<ScreenBackButton>().Single());
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden); AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
@ -885,7 +902,7 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddAssert("difficulty multiplier display shows correct value", AddAssert("difficulty multiplier display shows correct value",
() => modSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(0.1).Within(Precision.DOUBLE_EPSILON)); () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(0.1).Within(Precision.DOUBLE_EPSILON));
// this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation, // this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation,
// it is instrumental in the reproduction of the failure scenario that this test is supposed to cover. // it is instrumental in the reproduction of the failure scenario that this test is supposed to cover.
@ -895,7 +912,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single() AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single()
.ChildrenOfType<RevertToDefaultButton<double>>().Single().TriggerClick()); .ChildrenOfType<RevertToDefaultButton<double>>().Single().TriggerClick());
AddUntilStep("difficulty multiplier display shows correct value", AddUntilStep("difficulty multiplier display shows correct value",
() => modSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON)); () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON));
} }
[Test] [Test]
@ -1015,8 +1032,6 @@ namespace osu.Game.Tests.Visual.UserInterface
private partial class TestModSelectOverlay : UserModSelectOverlay private partial class TestModSelectOverlay : UserModSelectOverlay
{ {
protected override bool ShowPresets => true; protected override bool ShowPresets => true;
public new ShearedButton BackButton => base.BackButton;
} }
private class TestUnimplementedMod : Mod private class TestUnimplementedMod : Mod

View File

@ -76,6 +76,24 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddAssert("text selected", () => numberBoxes.First().SelectedText == "987654321"); AddAssert("text selected", () => numberBoxes.First().SelectedText == "987654321");
AddStep("click away", () =>
{
InputManager.MoveMouseTo(Vector2.Zero);
InputManager.Click(MouseButton.Left);
});
Drawable textContainer = null!;
AddStep("move mouse to end of text", () =>
{
textContainer = numberBoxes.First().ChildrenOfType<Container>().ElementAt(1);
InputManager.MoveMouseTo(textContainer.ScreenSpaceDrawQuad.TopRight);
});
AddStep("hold mouse", () => InputManager.PressButton(MouseButton.Left));
AddStep("drag to half", () => InputManager.MoveMouseTo(textContainer.ScreenSpaceDrawQuad.BottomRight - new Vector2(textContainer.ScreenSpaceDrawQuad.Width / 2 + 1f, 0)));
AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("half text selected", () => numberBoxes.First().SelectedText == "54321");
} }
private void clearTextboxes(IEnumerable<OsuTextBox> textBoxes) => AddStep("clear textbox", () => textBoxes.ForEach(textBox => textBox.Text = null)); private void clearTextboxes(IEnumerable<OsuTextBox> textBoxes) => AddStep("clear textbox", () => textBoxes.ForEach(textBox => textBox.Text = null));

View File

@ -0,0 +1,86 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public partial class TestSceneOsuTooltip : OsuManualInputManagerTestScene
{
private TestTooltipContainer container = null!;
private static readonly string[] test_case_tooltip_string =
[
"Hello!!",
string.Concat(Enumerable.Repeat("Hello ", 100)),
//TODO: o!f issue: https://github.com/ppy/osu-framework/issues/5007
//Enable after o!f fixed
// $"H{new string('e', 500)}llo",
];
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(100),
Children = new Drawable[]
{
new Box
{
Colour = Colour4.Red.Opacity(0.5f),
RelativeSizeAxes = Axes.Both,
},
container = new TestTooltipContainer
{
RelativeSizeAxes = Axes.Both,
Child = new OsuSpriteText
{
Text = "Hover me!",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 50)
}
},
},
};
});
[TestCaseSource(nameof(test_case_tooltip_string))]
public void TestTooltipBasic(string text)
{
AddStep("Set tooltip content", () => container.TooltipText = text);
AddStep("Move mouse to container", () => InputManager.MoveMouseTo(new Vector2(InputManager.ScreenSpaceDrawQuad.Centre.X, InputManager.ScreenSpaceDrawQuad.Centre.Y)));
OsuTooltipContainer.OsuTooltip? tooltip = null!;
AddUntilStep("Wait for the tooltip shown", () =>
{
tooltip = container.FindClosestParent<OsuTooltipContainer>().ChildrenOfType<OsuTooltipContainer.OsuTooltip>().FirstOrDefault();
return tooltip != null && tooltip.Alpha == 1;
});
AddAssert("Check tooltip is under width limit", () => tooltip != null && tooltip.Width <= 500);
}
internal sealed partial class TestTooltipContainer : Container, IHasTooltip
{
public LocalisableString TooltipText { get; set; }
}
}
}

View File

@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
private DependencyProvidingContainer contentContainer = null!; private DependencyProvidingContainer contentContainer = null!;
private ScreenFooter screenFooter = null!; private ScreenFooter screenFooter = null!;
private TestModSelectOverlay overlay = null!; private TestModSelectOverlay modOverlay = null!;
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}, },
Children = new Drawable[] Children = new Drawable[]
{ {
overlay = new TestModSelectOverlay(), modOverlay = new TestModSelectOverlay(),
new PopoverContainer new PopoverContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.UserInterface
screenFooter.SetButtons(new ScreenFooterButton[] screenFooter.SetButtons(new ScreenFooterButton[]
{ {
new ScreenFooterButtonMods(overlay) { Current = SelectedMods }, new ScreenFooterButtonMods(modOverlay) { Current = SelectedMods },
new ScreenFooterButtonRandom(), new ScreenFooterButtonRandom(),
new ScreenFooterButtonOptions(), new ScreenFooterButtonOptions(),
}); });
@ -178,6 +178,24 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("footer hidden", () => screenFooter.State.Value == Visibility.Hidden); AddAssert("footer hidden", () => screenFooter.State.Value == Visibility.Hidden);
} }
[Test]
public void TestLoadOverlayAfterFooterIsDisplayed()
{
TestShearedOverlayContainer externalOverlay = null!;
AddStep("show mod overlay", () => modOverlay.Show());
AddUntilStep("mod footer content shown", () => this.ChildrenOfType<ModSelectFooterContent>().SingleOrDefault()?.IsPresent, () => Is.True);
AddStep("add external overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer()));
AddUntilStep("wait for load", () => externalOverlay.IsLoaded);
AddAssert("mod footer content still shown", () => this.ChildrenOfType<ModSelectFooterContent>().SingleOrDefault()?.IsPresent, () => Is.True);
AddAssert("external overlay content not shown", () => this.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent, () => Is.Not.True);
AddStep("hide mod overlay", () => modOverlay.Hide());
AddUntilStep("mod footer content hidden", () => this.ChildrenOfType<ModSelectFooterContent>().SingleOrDefault()?.IsPresent, () => Is.Not.True);
AddAssert("external overlay content still not shown", () => this.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent, () => Is.Not.True);
}
private partial class TestModSelectOverlay : UserModSelectOverlay private partial class TestModSelectOverlay : UserModSelectOverlay
{ {
protected override bool ShowPresets => true; protected override bool ShowPresets => true;
@ -185,8 +203,6 @@ namespace osu.Game.Tests.Visual.UserInterface
private partial class TestShearedOverlayContainer : ShearedOverlayContainer private partial class TestShearedOverlayContainer : ShearedOverlayContainer
{ {
public override bool UseNewFooter => true;
public TestShearedOverlayContainer() public TestShearedOverlayContainer()
: base(OverlayColourScheme.Orange) : base(OverlayColourScheme.Orange)
{ {
@ -212,7 +228,7 @@ namespace osu.Game.Tests.Visual.UserInterface
return false; return false;
} }
public override Drawable CreateFooterContent() => new TestFooterContent(); public override VisibilityContainer CreateFooterContent() => new TestFooterContent();
public partial class TestFooterContent : VisibilityContainer public partial class TestFooterContent : VisibilityContainer
{ {

View File

@ -43,6 +43,8 @@ namespace osu.Game.Beatmaps
public override async Task<Live<BeatmapSetInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original) public override async Task<Live<BeatmapSetInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original)
{ {
var originalDateAdded = original.DateAdded;
Guid originalId = original.ID; Guid originalId = original.ID;
var imported = await Import(notification, new[] { importTask }).ConfigureAwait(false); var imported = await Import(notification, new[] { importTask }).ConfigureAwait(false);
@ -57,8 +59,11 @@ namespace osu.Game.Beatmaps
// If there were no changes, ensure we don't accidentally nuke ourselves. // If there were no changes, ensure we don't accidentally nuke ourselves.
if (first.ID == originalId) if (first.ID == originalId)
{ {
first.PerformRead(s => first.PerformWrite(s =>
{ {
// Transfer local values which should be persisted across a beatmap update.
s.DateAdded = originalDateAdded;
// Re-run processing even in this case. We might have outdated metadata. // Re-run processing even in this case. We might have outdated metadata.
ProcessBeatmap?.Invoke(s, MetadataLookupScope.OnlineFirst); ProcessBeatmap?.Invoke(s, MetadataLookupScope.OnlineFirst);
}); });
@ -79,7 +84,7 @@ namespace osu.Game.Beatmaps
original.DeletePending = true; original.DeletePending = true;
// Transfer local values which should be persisted across a beatmap update. // Transfer local values which should be persisted across a beatmap update.
updated.DateAdded = original.DateAdded; updated.DateAdded = originalDateAdded;
transferCollectionReferences(realm, original, updated); transferCollectionReferences(realm, original, updated);
@ -278,6 +283,9 @@ namespace osu.Game.Beatmaps
protected override void UndeleteForReuse(BeatmapSetInfo existing) protected override void UndeleteForReuse(BeatmapSetInfo existing)
{ {
if (!existing.DeletePending)
return;
base.UndeleteForReuse(existing); base.UndeleteForReuse(existing);
existing.DateAdded = DateTimeOffset.UtcNow; existing.DateAdded = DateTimeOffset.UtcNow;
} }

View File

@ -34,7 +34,7 @@ namespace osu.Game.Beatmaps.ControlPoints
set => ScrollSpeedBindable.Value = value; set => ScrollSpeedBindable.Value = value;
} }
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Purple; public override Color4 GetRepresentingColour(OsuColour colours) => colours.Orange1;
/// <summary> /// <summary>
/// Whether this control point enables Kiai mode. /// Whether this control point enables Kiai mode.

View File

@ -26,7 +26,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary> /// </summary>
private const double default_beat_length = 60000.0 / 60.0; private const double default_beat_length = 60000.0 / 60.0;
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Orange1; public override Color4 GetRepresentingColour(OsuColour colours) => colours.Red2;
public static readonly TimingControlPoint DEFAULT = new TimingControlPoint public static readonly TimingControlPoint DEFAULT = new TimingControlPoint
{ {

View File

@ -101,7 +101,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
bool shouldDim = Dimmed.Value || playButton.Playing.Value; bool shouldDim = Dimmed.Value || playButton.Playing.Value;
playButton.FadeTo(shouldDim ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); playButton.FadeTo(shouldDim ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
background.FadeColour(colourProvider.Background6.Opacity(shouldDim ? 0.8f : 0f), BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); background.FadeColour(colourProvider.Background6.Opacity(shouldDim ? 0.6f : 0f), BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
} }
} }
} }

View File

@ -43,6 +43,9 @@ namespace osu.Game.Collections
// //
// if we want to support user sorting (but changes will need to be made to realm to persist). // if we want to support user sorting (but changes will need to be made to realm to persist).
ShowDragHandle.Value = false; ShowDragHandle.Value = false;
Masking = true;
CornerRadius = item_height / 2;
} }
protected override Drawable CreateContent() => new ItemContent(Model); protected override Drawable CreateContent() => new ItemContent(Model);
@ -50,7 +53,7 @@ namespace osu.Game.Collections
/// <summary> /// <summary>
/// The main content of the <see cref="DrawableCollectionListItem"/>. /// The main content of the <see cref="DrawableCollectionListItem"/>.
/// </summary> /// </summary>
private partial class ItemContent : CircularContainer private partial class ItemContent : CompositeDrawable
{ {
private readonly Live<BeatmapCollection> collection; private readonly Live<BeatmapCollection> collection;
@ -65,13 +68,12 @@ namespace osu.Game.Collections
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = item_height; Height = item_height;
Masking = true;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Children = new[] InternalChildren = new[]
{ {
collection.IsManaged collection.IsManaged
? new DeleteButton(collection) ? new DeleteButton(collection)
@ -132,7 +134,7 @@ namespace osu.Game.Collections
} }
} }
public partial class DeleteButton : CompositeDrawable public partial class DeleteButton : OsuClickableContainer
{ {
public Func<Vector2, bool> IsTextBoxHovered = null!; public Func<Vector2, bool> IsTextBoxHovered = null!;
@ -155,7 +157,7 @@ namespace osu.Game.Collections
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
InternalChild = fadeContainer = new Container Child = fadeContainer = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Alpha = 0.1f, Alpha = 0.1f,
@ -176,6 +178,14 @@ namespace osu.Game.Collections
} }
} }
}; };
Action = () =>
{
if (collection.PerformRead(c => c.BeatmapMD5Hashes.Count) == 0)
deleteCollection();
else
dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection));
};
} }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && !IsTextBoxHovered(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && !IsTextBoxHovered(screenSpacePos);
@ -195,12 +205,7 @@ namespace osu.Game.Collections
{ {
background.FlashColour(Color4.White, 150); background.FlashColour(Color4.White, 150);
if (collection.PerformRead(c => c.BeatmapMD5Hashes.Count) == 0) return base.OnClick(e);
deleteCollection();
else
dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection));
return true;
} }
private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c)); private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c));

View File

@ -389,7 +389,7 @@ namespace osu.Game.Database
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>( HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(
r.All<ScoreInfo>() r.All<ScoreInfo>()
.Where(s => s.TotalScoreVersion < 30000013) // last total score version with a significant change to ranks .Where(s => s.TotalScoreVersion < 30000013 && !s.BackgroundReprocessingFailed) // last total score version with a significant change to ranks
.AsEnumerable() .AsEnumerable()
// must be done after materialisation, as realm doesn't support // must be done after materialisation, as realm doesn't support
// filtering on nested property predicates or projection via `.Select()` // filtering on nested property predicates or projection via `.Select()`

View File

@ -64,6 +64,7 @@ namespace osu.Game.Graphics.Containers
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new HoverClickSounds(),
new GridContainer new GridContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -92,7 +93,6 @@ namespace osu.Game.Graphics.Containers
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
}, },
new HoverClickSounds()
}; };
} }

View File

@ -10,7 +10,7 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites; using osu.Framework.Graphics.Containers;
namespace osu.Game.Graphics.Cursor namespace osu.Game.Graphics.Cursor
{ {
@ -27,15 +27,20 @@ namespace osu.Game.Graphics.Cursor
public partial class OsuTooltip : Tooltip public partial class OsuTooltip : Tooltip
{ {
private const float max_width = 500;
private readonly Box background; private readonly Box background;
private readonly OsuSpriteText text; private readonly TextFlowContainer text;
private bool instantMovement = true; private bool instantMovement = true;
public override void SetContent(LocalisableString contentString) private LocalisableString lastContent;
{
if (contentString == text.Text) return;
text.Text = contentString; public override void SetContent(LocalisableString content)
{
if (content.Equals(lastContent))
return;
text.Text = content;
if (IsPresent) if (IsPresent)
{ {
@ -44,6 +49,8 @@ namespace osu.Game.Graphics.Cursor
} }
else else
AutoSizeDuration = 0; AutoSizeDuration = 0;
lastContent = content;
} }
public OsuTooltip() public OsuTooltip()
@ -65,10 +72,14 @@ namespace osu.Game.Graphics.Cursor
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Alpha = 0.9f, Alpha = 0.9f,
}, },
text = new OsuSpriteText text = new TextFlowContainer(f =>
{ {
Padding = new MarginPadding(5), f.Font = OsuFont.GetFont(weight: FontWeight.Regular);
Font = OsuFont.GetFont(weight: FontWeight.Regular) })
{
Margin = new MarginPadding(5),
AutoSizeAxes = Axes.Both,
MaximumSize = new Vector2(max_width, float.PositiveInfinity),
} }
}; };
} }

View File

@ -16,15 +16,9 @@ namespace osu.Game.Graphics.UserInterface
[Description("button-sidebar")] [Description("button-sidebar")]
ButtonSidebar, ButtonSidebar,
[Description("toolbar")]
Toolbar,
[Description("tabselect")] [Description("tabselect")]
TabSelect, TabSelect,
[Description("scrolltotop")]
ScrollToTop,
[Description("dialog-cancel")] [Description("dialog-cancel")]
DialogCancel, DialogCancel,

View File

@ -122,8 +122,6 @@ namespace osu.Game.Graphics.UserInterface
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
} }
}; };
Padding = new MarginPadding { Left = 5, Right = 5 };
} }
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)

View File

@ -261,7 +261,8 @@ namespace osu.Game.Graphics.UserInterface
base.OnFocus(e); base.OnFocus(e);
if (SelectAllOnFocus) // we may become focused from an ongoing drag operation, we don't want to overwrite selection in that case.
if (SelectAllOnFocus && string.IsNullOrEmpty(SelectedText))
SelectAll(); SelectAll();
} }

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class DailyChallengeStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.DailyChallenge";
/// <summary>
/// "Today&#39;s daily challenge has concluded thanks for playing!
///
/// Tomorrow&#39;s challenge is now being prepared and will appear soon."
/// </summary>
public static LocalisableString ChallengeEndedNotification => new TranslatableString(getKey(@"todays_daily_challenge_has_concluded"),
@"Today's daily challenge has concluded thanks for playing!
Tomorrow's challenge is now being prepared and will appear soon.");
/// <summary>
/// "Today&#39;s daily challenge is now live! Click here to play."
/// </summary>
public static LocalisableString ChallengeLiveNotification => new TranslatableString(getKey(@"todays_daily_challenge_is_now"), @"Today's daily challenge is now live! Click here to play.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -118,12 +118,11 @@ namespace osu.Game.Online.API
u.OldValue?.Activity.UnbindFrom(activity); u.OldValue?.Activity.UnbindFrom(activity);
u.NewValue.Activity.BindTo(activity); u.NewValue.Activity.BindTo(activity);
if (u.OldValue != null) u.OldValue?.Status.UnbindFrom(localUserStatus);
localUserStatus.UnbindFrom(u.OldValue.Status); u.NewValue.Status.BindTo(localUserStatus);
localUserStatus.BindTo(u.NewValue.Status);
}, true); }, true);
localUserStatus.BindValueChanged(val => configStatus.Value = val.NewValue); localUserStatus.BindTo(configStatus);
var thread = new Thread(run) var thread = new Thread(run)
{ {
@ -164,6 +163,8 @@ namespace osu.Game.Online.API
public string AccessToken => authentication.RequestAccessToken(); public string AccessToken => authentication.RequestAccessToken();
public Guid SessionIdentifier { get; } = Guid.NewGuid();
/// <summary> /// <summary>
/// Number of consecutive requests which failed due to network issues. /// Number of consecutive requests which failed due to network issues.
/// </summary> /// </summary>
@ -598,6 +599,7 @@ namespace osu.Game.Online.API
password = null; password = null;
SecondFactorCode = null; SecondFactorCode = null;
authentication.Clear(); authentication.Clear();
configStatus.Value = UserStatus.Online;
// Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present // Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present
Schedule(() => Schedule(() =>

View File

@ -39,6 +39,8 @@ namespace osu.Game.Online.API
public string AccessToken => "token"; public string AccessToken => "token";
public Guid SessionIdentifier { get; } = Guid.NewGuid();
/// <seealso cref="APIAccess.IsLoggedIn"/> /// <seealso cref="APIAccess.IsLoggedIn"/>
public bool IsLoggedIn => State.Value > APIState.Offline; public bool IsLoggedIn => State.Value > APIState.Offline;

View File

@ -44,6 +44,12 @@ namespace osu.Game.Online.API
/// </summary> /// </summary>
string AccessToken { get; } string AccessToken { get; }
/// <summary>
/// Used as an identifier of a single local lazer session.
/// Sent across the wire for the purposes of concurrency control to spectator server.
/// </summary>
Guid SessionIdentifier { get; }
/// <summary> /// <summary>
/// Returns whether the local user is logged in. /// Returns whether the local user is logged in.
/// </summary> /// </summary>

View File

@ -201,6 +201,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"playmode")] [JsonProperty(@"playmode")]
public string PlayMode; public string PlayMode;
[JsonProperty(@"profile_hue")]
public int? ProfileHue;
[JsonProperty(@"profile_order")] [JsonProperty(@"profile_order")]
public string[] ProfileOrder; public string[] ProfileOrder;
@ -269,6 +272,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty("groups")] [JsonProperty("groups")]
public APIUserGroup[] Groups; public APIUserGroup[] Groups;
[JsonProperty("daily_challenge_user_stats")]
public APIUserDailyChallengeStatistics DailyChallengeStatistics = new APIUserDailyChallengeStatistics();
public override string ToString() => Username; public override string ToString() => Username;
/// <summary> /// <summary>

View File

@ -0,0 +1,41 @@
// 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 Newtonsoft.Json;
namespace osu.Game.Online.API.Requests.Responses
{
public class APIUserDailyChallengeStatistics
{
[JsonProperty("user_id")]
public int UserID;
[JsonProperty("daily_streak_best")]
public int DailyStreakBest;
[JsonProperty("daily_streak_current")]
public int DailyStreakCurrent;
[JsonProperty("weekly_streak_best")]
public int WeeklyStreakBest;
[JsonProperty("weekly_streak_current")]
public int WeeklyStreakCurrent;
[JsonProperty("top_10p_placements")]
public int Top10PercentPlacements;
[JsonProperty("top_50p_placements")]
public int Top50PercentPlacements;
[JsonProperty("playcount")]
public int PlayCount;
[JsonProperty("last_update")]
public DateTimeOffset? LastUpdate;
[JsonProperty("last_weekly_streak")]
public DateTimeOffset? LastWeeklyStreak;
}
}

View File

@ -178,7 +178,7 @@ namespace osu.Game.Online.Chat
protected partial class StandAloneDaySeparator : DaySeparator protected partial class StandAloneDaySeparator : DaySeparator
{ {
protected override float TextSize => 14; protected override float TextSize => 13;
protected override float LineHeight => 1; protected override float LineHeight => 1;
protected override float Spacing => 5; protected override float Spacing => 5;
protected override float DateAlign => 125; protected override float DateAlign => 125;
@ -198,9 +198,9 @@ namespace osu.Game.Online.Chat
protected partial class StandAloneMessage : ChatLine protected partial class StandAloneMessage : ChatLine
{ {
protected override float FontSize => 15; protected override float FontSize => 13;
protected override float Spacing => 5; protected override float Spacing => 5;
protected override float UsernameWidth => 75; protected override float UsernameWidth => 90;
public StandAloneMessage(Message message) public StandAloneMessage(Message message)
: base(message) : base(message)

View File

@ -19,6 +19,9 @@ namespace osu.Game.Online
{ {
public const string SERVER_SHUTDOWN_MESSAGE = "Server is shutting down."; public const string SERVER_SHUTDOWN_MESSAGE = "Server is shutting down.";
public const string VERSION_HASH_HEADER = @"X-Osu-Version-Hash";
public const string CLIENT_SESSION_ID_HEADER = @"X-Client-Session-ID";
/// <summary> /// <summary>
/// Invoked whenever a new hub connection is built, to configure it before it's started. /// Invoked whenever a new hub connection is built, to configure it before it's started.
/// </summary> /// </summary>
@ -68,8 +71,11 @@ namespace osu.Game.Online
options.Proxy.Credentials = CredentialCache.DefaultCredentials; options.Proxy.Credentials = CredentialCache.DefaultCredentials;
} }
options.Headers.Add("Authorization", $"Bearer {API.AccessToken}"); options.Headers.Add(@"Authorization", @$"Bearer {API.AccessToken}");
options.Headers.Add("OsuVersionHash", versionHash); // non-standard header name kept for backwards compatibility, can be removed after server side has migrated to `VERSION_HASH_HEADER`
options.Headers.Add(@"OsuVersionHash", versionHash);
options.Headers.Add(VERSION_HASH_HEADER, versionHash);
options.Headers.Add(CLIENT_SESSION_ID_HEADER, API.SessionIdentifier.ToString());
}); });
if (RuntimeFeature.IsDynamicCodeCompiled && preferMessagePack) if (RuntimeFeature.IsDynamicCodeCompiled && preferMessagePack)

View File

@ -25,5 +25,17 @@ namespace osu.Game.Online.Metadata
/// </summary> /// </summary>
[Key(1)] [Key(1)]
public long[] TotalScoreDistribution { get; set; } = new long[TOTAL_SCORE_DISTRIBUTION_BINS]; public long[] TotalScoreDistribution { get; set; } = new long[TOTAL_SCORE_DISTRIBUTION_BINS];
/// <summary>
/// The cumulative total of all passing scores (across all users) for the playlist item so far.
/// </summary>
[Key(2)]
public long CumulativeScore { get; set; }
/// <summary>
/// The last score to have been processed into provided statistics. Generally only for server-side accounting purposes.
/// </summary>
[Key(3)]
public ulong LastProcessedScoreID { get; set; }
} }
} }

View File

@ -215,6 +215,7 @@ namespace osu.Game.Online.Metadata
Debug.Assert(connection != null); Debug.Assert(connection != null);
await connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence)).ConfigureAwait(false); await connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence)).ConfigureAwait(false);
Schedule(() => isWatchingUserPresence.Value = true); Schedule(() => isWatchingUserPresence.Value = true);
Logger.Log($@"{nameof(OnlineMetadataClient)} began watching user presence", LoggingTarget.Network);
} }
public override async Task EndWatchingUserPresence() public override async Task EndWatchingUserPresence()
@ -228,6 +229,7 @@ namespace osu.Game.Online.Metadata
Schedule(() => userStates.Clear()); Schedule(() => userStates.Clear());
Debug.Assert(connection != null); Debug.Assert(connection != null);
await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false); await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false);
Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network);
} }
finally finally
{ {
@ -247,7 +249,9 @@ namespace osu.Game.Online.Metadata
throw new OperationCanceledException(); throw new OperationCanceledException();
Debug.Assert(connection != null); Debug.Assert(connection != null);
return await connection.InvokeAsync<MultiplayerPlaylistItemStats[]>(nameof(IMetadataServer.BeginWatchingMultiplayerRoom), id).ConfigureAwait(false); var result = await connection.InvokeAsync<MultiplayerPlaylistItemStats[]>(nameof(IMetadataServer.BeginWatchingMultiplayerRoom), id).ConfigureAwait(false);
Logger.Log($@"{nameof(OnlineMetadataClient)} began watching multiplayer room with ID {id}", LoggingTarget.Network);
return result;
} }
public override async Task EndWatchingMultiplayerRoom(long id) public override async Task EndWatchingMultiplayerRoom(long id)
@ -257,6 +261,7 @@ namespace osu.Game.Online.Metadata
Debug.Assert(connection != null); Debug.Assert(connection != null);
await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingMultiplayerRoom), id).ConfigureAwait(false); await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingMultiplayerRoom), id).ConfigureAwait(false);
Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching multiplayer room with ID {id}", LoggingTarget.Network);
} }
public override async Task DisconnectRequested() public override async Task DisconnectRequested()

View File

@ -46,6 +46,9 @@ namespace osu.Game.Online.Rooms
[JsonProperty("statistics")] [JsonProperty("statistics")]
public Dictionary<HitResult, int> Statistics = new Dictionary<HitResult, int>(); public Dictionary<HitResult, int> Statistics = new Dictionary<HitResult, int>();
[JsonProperty("maximum_statistics")]
public Dictionary<HitResult, int> MaximumStatistics = new Dictionary<HitResult, int>();
[JsonProperty("passed")] [JsonProperty("passed")]
public bool Passed { get; set; } public bool Passed { get; set; }
@ -58,9 +61,15 @@ namespace osu.Game.Online.Rooms
[JsonProperty("position")] [JsonProperty("position")]
public int? Position { get; set; } public int? Position { get; set; }
[JsonProperty("pp")]
public double? PP { get; set; }
[JsonProperty("has_replay")] [JsonProperty("has_replay")]
public bool HasReplay { get; set; } public bool HasReplay { get; set; }
[JsonProperty("ranked")]
public bool Ranked { get; set; }
/// <summary> /// <summary>
/// Any scores in the room around this score. /// Any scores in the room around this score.
/// </summary> /// </summary>
@ -83,13 +92,17 @@ namespace osu.Game.Online.Rooms
MaxCombo = MaxCombo, MaxCombo = MaxCombo,
BeatmapInfo = beatmap, BeatmapInfo = beatmap,
Ruleset = rulesets.GetRuleset(playlistItem.RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {playlistItem.RulesetID} not found locally"), Ruleset = rulesets.GetRuleset(playlistItem.RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {playlistItem.RulesetID} not found locally"),
Passed = Passed,
Statistics = Statistics, Statistics = Statistics,
MaximumStatistics = MaximumStatistics,
User = User, User = User,
Accuracy = Accuracy, Accuracy = Accuracy,
Date = EndedAt, Date = EndedAt,
HasOnlineReplay = HasReplay, HasOnlineReplay = HasReplay,
Rank = Rank, Rank = Rank,
Mods = Mods?.Select(m => m.ToMod(rulesetInstance)).ToArray() ?? Array.Empty<Mod>(), Mods = Mods?.Select(m => m.ToMod(rulesetInstance)).ToArray() ?? Array.Empty<Mod>(),
PP = PP,
Ranked = Ranked,
Position = Position, Position = Position,
}; };

View File

@ -51,6 +51,7 @@ using osu.Game.Online.Chat;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Music; using osu.Game.Overlays.Music;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.SkinEditor; using osu.Game.Overlays.SkinEditor;
@ -62,6 +63,7 @@ using osu.Game.Screens;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Footer; using osu.Game.Screens.Footer;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
@ -83,7 +85,7 @@ namespace osu.Game
public partial class OsuGame : OsuGameBase, IKeyBindingHandler<GlobalAction>, ILocalUserPlayInfo, IPerformFromScreenRunner, IOverlayManager, ILinkHandler public partial class OsuGame : OsuGameBase, IKeyBindingHandler<GlobalAction>, ILocalUserPlayInfo, IPerformFromScreenRunner, IOverlayManager, ILinkHandler
{ {
#if DEBUG #if DEBUG
// Different port allows runnning release and debug builds alongside each other. // Different port allows running release and debug builds alongside each other.
public const int IPC_PORT = 44824; public const int IPC_PORT = 44824;
#else #else
public const int IPC_PORT = 44823; public const int IPC_PORT = 44823;
@ -132,6 +134,8 @@ namespace osu.Game
private Container topMostOverlayContent; private Container topMostOverlayContent;
private Container footerBasedOverlayContent;
protected ScalingContainer ScreenContainer { get; private set; } protected ScalingContainer ScreenContainer { get; private set; }
protected Container ScreenOffsetContainer { get; private set; } protected Container ScreenOffsetContainer { get; private set; }
@ -156,8 +160,6 @@ namespace osu.Game
private float toolbarOffset => (Toolbar?.Position.Y ?? 0) + (Toolbar?.DrawHeight ?? 0); private float toolbarOffset => (Toolbar?.Position.Y ?? 0) + (Toolbar?.DrawHeight ?? 0);
private float screenFooterOffset => (ScreenFooter?.DrawHeight ?? 0) - (ScreenFooter?.Position.Y ?? 0);
private IdleTracker idleTracker; private IdleTracker idleTracker;
/// <summary> /// <summary>
@ -242,6 +244,10 @@ namespace osu.Game
throw new ArgumentException($@"{overlayContainer} has already been registered via {nameof(IOverlayManager.RegisterBlockingOverlay)} once."); throw new ArgumentException($@"{overlayContainer} has already been registered via {nameof(IOverlayManager.RegisterBlockingOverlay)} once.");
externalOverlays.Add(overlayContainer); externalOverlays.Add(overlayContainer);
if (overlayContainer is ShearedOverlayContainer)
footerBasedOverlayContent.Add(overlayContainer);
else
overlayContent.Add(overlayContainer); overlayContent.Add(overlayContainer);
if (overlayContainer is OsuFocusedOverlayContainer focusedOverlayContainer) if (overlayContainer is OsuFocusedOverlayContainer focusedOverlayContainer)
@ -744,22 +750,35 @@ namespace osu.Game
return; return;
} }
// This should be able to be performed from song select, but that is disabled for now // This should be able to be performed from song select always, but that is disabled for now
// due to the weird decoupled ruleset logic (which can cause a crash in certain filter scenarios). // due to the weird decoupled ruleset logic (which can cause a crash in certain filter scenarios).
// //
// As a special case, if the beatmap and ruleset already match, allow immediately displaying the score from song select. // As a special case, if the beatmap and ruleset already match, allow immediately displaying the score from song select.
// This is guaranteed to not crash, and feels better from a user's perspective (ie. if they are clicking a score in the // This is guaranteed to not crash, and feels better from a user's perspective (ie. if they are clicking a score in the
// song select leaderboard). // song select leaderboard).
// Similar exemptions are made here for daily challenge where it is guaranteed that beatmap and ruleset match.
// `OnlinePlayScreen` is excluded because when resuming back to it,
// `RoomSubScreen` changes the global beatmap to the next playlist item on resume,
// which may not match the score, and thus crash.
IEnumerable<Type> validScreens = IEnumerable<Type> validScreens =
Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap) && Ruleset.Value.Equals(databasedScore.ScoreInfo.Ruleset) Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap) && Ruleset.Value.Equals(databasedScore.ScoreInfo.Ruleset)
? new[] { typeof(SongSelect) } ? new[] { typeof(SongSelect), typeof(DailyChallenge) }
: Array.Empty<Type>(); : Array.Empty<Type>();
PerformFromScreen(screen => PerformFromScreen(screen =>
{ {
Logger.Log($"{nameof(PresentScore)} updating beatmap ({databasedBeatmap}) and ruleset ({databasedScore.ScoreInfo.Ruleset}) to match score"); Logger.Log($"{nameof(PresentScore)} updating beatmap ({databasedBeatmap}) and ruleset ({databasedScore.ScoreInfo.Ruleset}) to match score");
// some screens (mostly online) disable the ruleset/beatmap bindable.
// attempting to set the ruleset/beatmap in that state will crash.
// however, the `validScreens` pre-check above should ensure that we actually never come from one of those screens
// while simultaneously having mismatched ruleset/beatmap.
// therefore this is just a safety against touching the possibly-disabled bindables if we don't actually have to touch them.
// if it ever fails, then this probably *should* crash anyhow (so that we can fix it).
if (!Ruleset.Value.Equals(databasedScore.ScoreInfo.Ruleset))
Ruleset.Value = databasedScore.ScoreInfo.Ruleset; Ruleset.Value = databasedScore.ScoreInfo.Ruleset;
if (!Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap))
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap); Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap);
switch (presentType) switch (presentType)
@ -934,7 +953,6 @@ namespace osu.Game
return string.Join(" / ", combinations); return string.Join(" / ", combinations);
}; };
Container logoContainer;
ScreenFooter.BackReceptor backReceptor; ScreenFooter.BackReceptor backReceptor;
dependencies.CacheAs(idleTracker = new GameIdleTracker(6000)); dependencies.CacheAs(idleTracker = new GameIdleTracker(6000));
@ -948,6 +966,8 @@ namespace osu.Game
Add(sessionIdleTracker); Add(sessionIdleTracker);
Container logoContainer;
AddRange(new Drawable[] AddRange(new Drawable[]
{ {
new VolumeControlReceptor new VolumeControlReceptor
@ -976,11 +996,19 @@ namespace osu.Game
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
Action = () => ScreenFooter.OnBack?.Invoke(), Action = () => ScreenFooter.OnBack?.Invoke(),
}, },
logoContainer = new Container { RelativeSizeAxes = Axes.Both },
footerBasedOverlayContent = new Container
{
Depth = -1,
RelativeSizeAxes = Axes.Both,
},
new PopoverContainer new PopoverContainer
{ {
Depth = -1,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = ScreenFooter = new ScreenFooter(backReceptor) Child = ScreenFooter = new ScreenFooter(backReceptor)
{ {
RequestLogoInFront = inFront => ScreenContainer.ChangeChildDepth(logoContainer, inFront ? float.MinValue : 0),
OnBack = () => OnBack = () =>
{ {
if (!(ScreenStack.CurrentScreen is IOsuScreen currentScreen)) if (!(ScreenStack.CurrentScreen is IOsuScreen currentScreen))
@ -991,7 +1019,6 @@ namespace osu.Game
} }
}, },
}, },
logoContainer = new Container { RelativeSizeAxes = Axes.Both },
} }
}, },
} }
@ -1025,7 +1052,7 @@ namespace osu.Game
if (!IsDeployedBuild) if (!IsDeployedBuild)
{ {
dependencies.Cache(versionManager = new VersionManager { Depth = int.MinValue }); dependencies.Cache(versionManager = new VersionManager());
loadComponentSingleFile(versionManager, ScreenContainer.Add); loadComponentSingleFile(versionManager, ScreenContainer.Add);
} }
@ -1072,7 +1099,7 @@ namespace osu.Game
loadComponentSingleFile(CreateUpdateManager(), Add, true); loadComponentSingleFile(CreateUpdateManager(), Add, true);
// overlay elements // overlay elements
loadComponentSingleFile(FirstRunOverlay = new FirstRunSetupOverlay(), overlayContent.Add, true); loadComponentSingleFile(FirstRunOverlay = new FirstRunSetupOverlay(), footerBasedOverlayContent.Add, true);
loadComponentSingleFile(new ManageCollectionsDialog(), overlayContent.Add, true); loadComponentSingleFile(new ManageCollectionsDialog(), overlayContent.Add, true);
loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true); loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true);
loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true); loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
@ -1137,7 +1164,7 @@ namespace osu.Game
} }
// ensure only one of these overlays are open at once. // ensure only one of these overlays are open at once.
var singleDisplayOverlays = new OverlayContainer[] { FirstRunOverlay, chatOverlay, news, dashboard, beatmapListing, changelogOverlay, rankingsOverlay, wikiOverlay }; var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, news, dashboard, beatmapListing, changelogOverlay, rankingsOverlay, wikiOverlay };
foreach (var overlay in singleDisplayOverlays) foreach (var overlay in singleDisplayOverlays)
{ {
@ -1485,7 +1512,6 @@ namespace osu.Game
ScreenOffsetContainer.Padding = new MarginPadding { Top = toolbarOffset }; ScreenOffsetContainer.Padding = new MarginPadding { Top = toolbarOffset };
overlayOffsetContainer.Padding = new MarginPadding { Top = toolbarOffset }; overlayOffsetContainer.Padding = new MarginPadding { Top = toolbarOffset };
ScreenStack.Padding = new MarginPadding { Bottom = screenFooterOffset };
float horizontalOffset = 0f; float horizontalOffset = 0f;

View File

@ -20,8 +20,8 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat; using osu.Game.Online.Chat;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using Message = osu.Game.Online.Chat.Message;
namespace osu.Game.Overlays.Chat namespace osu.Game.Overlays.Chat
{ {
@ -47,11 +47,11 @@ namespace osu.Game.Overlays.Chat
public IReadOnlyCollection<Drawable> DrawableContentFlow => drawableContentFlow; public IReadOnlyCollection<Drawable> DrawableContentFlow => drawableContentFlow;
protected virtual float FontSize => 14; protected virtual float FontSize => 12;
protected virtual float Spacing => 15; protected virtual float Spacing => 15;
protected virtual float UsernameWidth => 130; protected virtual float UsernameWidth => 150;
[Resolved] [Resolved]
private ChannelManager? chatManager { get; set; } private ChannelManager? chatManager { get; set; }
@ -69,6 +69,41 @@ namespace osu.Game.Overlays.Chat
private Container? highlight; private Container? highlight;
private Drawable? background;
private bool alternatingBackground;
private bool requiresTimestamp = true;
public bool RequiresTimestamp
{
get => requiresTimestamp;
set
{
if (requiresTimestamp == value)
return;
requiresTimestamp = value;
if (!IsLoaded)
return;
updateMessageContent();
}
}
public bool AlternatingBackground
{
get => alternatingBackground;
set
{
if (alternatingBackground == value)
return;
alternatingBackground = value;
updateBackground();
}
}
/// <summary> /// <summary>
/// The colour used to paint the author's username. /// The colour used to paint the author's username.
/// </summary> /// </summary>
@ -102,14 +137,36 @@ namespace osu.Game.Overlays.Chat
configManager.BindWith(OsuSetting.Prefer24HourTime, prefer24HourTime); configManager.BindWith(OsuSetting.Prefer24HourTime, prefer24HourTime);
prefer24HourTime.BindValueChanged(_ => updateTimestamp()); prefer24HourTime.BindValueChanged(_ => updateTimestamp());
InternalChild = new GridContainer Padding = new MarginPadding { Right = 5 };
InternalChildren = new[]
{ {
background = new Container
{
Masking = true,
CornerRadius = 4,
Alpha = 0,
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Child = new Box
{
Colour = Colour4.FromHex("#3b3234"),
RelativeSizeAxes = Axes.Both,
},
},
new GridContainer
{
Padding = new MarginPadding
{
Horizontal = 2,
Vertical = 2,
},
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
ColumnDimensions = new[] ColumnDimensions = new[]
{ {
new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.Absolute, 45),
new Dimension(GridSizeMode.Absolute, Spacing + UsernameWidth + Spacing), new Dimension(GridSizeMode.Absolute, Spacing + UsernameWidth + Spacing),
new Dimension(), new Dimension(),
}, },
@ -120,9 +177,10 @@ namespace osu.Game.Overlays.Chat
drawableTimestamp = new OsuSpriteText drawableTimestamp = new OsuSpriteText
{ {
Shadow = false, Shadow = false,
Anchor = Anchor.CentreLeft, Anchor = Anchor.TopLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.TopLeft,
Font = OsuFont.GetFont(size: FontSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true), Spacing = new Vector2(-1, 0),
Font = OsuFont.GetFont(size: FontSize, weight: FontWeight.SemiBold, fixedWidth: true),
AlwaysPresent = true, AlwaysPresent = true,
}, },
drawableUsername = new DrawableChatUsername(message.Sender) drawableUsername = new DrawableChatUsername(message.Sender)
@ -143,7 +201,10 @@ namespace osu.Game.Overlays.Chat
} }
}, },
} }
}
}; };
updateBackground();
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -203,9 +264,17 @@ namespace osu.Game.Overlays.Chat
private void updateMessageContent() private void updateMessageContent()
{ {
this.FadeTo(message is LocalEchoMessage ? 0.4f : 1.0f, 500, Easing.OutQuint); this.FadeTo(message is LocalEchoMessage ? 0.4f : 1.0f, 500, Easing.OutQuint);
drawableTimestamp.FadeTo(message is LocalEchoMessage ? 0 : 1, 500, Easing.OutQuint);
if (requiresTimestamp && !(message is LocalEchoMessage))
{
drawableTimestamp.Show();
updateTimestamp(); updateTimestamp();
}
else
{
drawableTimestamp.Hide();
}
drawableUsername.Text = $@"{message.Sender.Username}"; drawableUsername.Text = $@"{message.Sender.Username}";
// remove non-existent channels from the link list // remove non-existent channels from the link list
@ -217,7 +286,7 @@ namespace osu.Game.Overlays.Chat
private void updateTimestamp() private void updateTimestamp()
{ {
drawableTimestamp.Text = message.Timestamp.LocalDateTime.ToLocalisableString(prefer24HourTime.Value ? @"HH:mm:ss" : @"hh:mm:ss tt"); drawableTimestamp.Text = message.Timestamp.LocalDateTime.ToLocalisableString(prefer24HourTime.Value ? @"HH:mm" : @"hh:mm tt");
} }
private static readonly Color4[] default_username_colours = private static readonly Color4[] default_username_colours =
@ -258,5 +327,11 @@ namespace osu.Game.Overlays.Chat
Color4Extensions.FromHex("812a96"), Color4Extensions.FromHex("812a96"),
Color4Extensions.FromHex("992861"), Color4Extensions.FromHex("992861"),
}; };
private void updateBackground()
{
if (background != null)
background.Alpha = alternatingBackground ? 0.2f : 0;
}
} }
} }

View File

@ -14,7 +14,7 @@ namespace osu.Game.Overlays.Chat
{ {
public partial class DaySeparator : Container public partial class DaySeparator : Container
{ {
protected virtual float TextSize => 15; protected virtual float TextSize => 13;
protected virtual float LineHeight => 2; protected virtual float LineHeight => 2;

View File

@ -61,7 +61,7 @@ namespace osu.Game.Overlays.Chat
Padding = new MarginPadding { Bottom = 5 }, Padding = new MarginPadding { Bottom = 5 },
Child = ChatLineFlow = new FillFlowContainer Child = ChatLineFlow = new FillFlowContainer
{ {
Padding = new MarginPadding { Horizontal = 10 }, Padding = new MarginPadding { Left = 3, Right = 10 },
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
@ -84,6 +84,25 @@ namespace osu.Game.Overlays.Chat
highlightedMessage.BindValueChanged(_ => processMessageHighlighting(), true); highlightedMessage.BindValueChanged(_ => processMessageHighlighting(), true);
} }
protected override void Update()
{
base.Update();
long? lastMinutes = null;
for (int i = 0; i < ChatLineFlow.Count; i++)
{
if (ChatLineFlow[i] is ChatLine chatline)
{
long minutes = chatline.Message.Timestamp.ToUnixTimeSeconds() / 60;
chatline.AlternatingBackground = i % 2 == 0;
chatline.RequiresTimestamp = minutes != lastMinutes;
lastMinutes = minutes;
}
}
}
/// <summary> /// <summary>
/// Processes any pending message in <see cref="highlightedMessage"/>. /// Processes any pending message in <see cref="highlightedMessage"/>.
/// </summary> /// </summary>

View File

@ -67,7 +67,7 @@ namespace osu.Game.Overlays.Dialog
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
Progress.BindValueChanged(progressChanged, true); Progress.BindValueChanged(progressChanged);
} }
protected override void Confirm() protected override void Confirm()
@ -114,13 +114,13 @@ namespace osu.Game.Overlays.Dialog
if (progress.NewValue < progress.OldValue) if (progress.NewValue < progress.OldValue)
return; return;
if (Clock.CurrentTime - lastTickPlaybackTime < 30) if (Clock.CurrentTime - lastTickPlaybackTime < 40)
return; return;
var channel = tickSample.GetChannel(); var channel = tickSample.GetChannel();
channel.Frequency.Value = 1 + progress.NewValue * 0.5f; channel.Frequency.Value = 1 + progress.NewValue;
channel.Volume.Value = 0.5f + progress.NewValue / 2f; channel.Volume.Value = 0.1f + progress.NewValue / 2f;
channel.Play(); channel.Play();

View File

@ -23,6 +23,7 @@ using osu.Game.Overlays.Settings;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Screens; using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -153,6 +154,7 @@ namespace osu.Game.Overlays.FirstRunSetup
OsuScreenStack stack; OsuScreenStack stack;
OsuLogo logo; OsuLogo logo;
ScreenFooter footer;
Padding = new MarginPadding(5); Padding = new MarginPadding(5);
@ -166,7 +168,8 @@ namespace osu.Game.Overlays.FirstRunSetup
{ {
RelativePositionAxes = Axes.Both, RelativePositionAxes = Axes.Both,
Position = new Vector2(0.5f), Position = new Vector2(0.5f),
}) }),
(typeof(ScreenFooter), footer = new ScreenFooter()),
}, },
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
@ -178,7 +181,8 @@ namespace osu.Game.Overlays.FirstRunSetup
Children = new Drawable[] Children = new Drawable[]
{ {
stack = new OsuScreenStack(), stack = new OsuScreenStack(),
logo footer,
logo,
}, },
}, },
} }

View File

@ -26,6 +26,7 @@ using osu.Game.Overlays.FirstRunSetup;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Screens; using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
namespace osu.Game.Overlays namespace osu.Game.Overlays
@ -44,8 +45,7 @@ namespace osu.Game.Overlays
private ScreenStack? stack; private ScreenStack? stack;
public ShearedButton NextButton = null!; public ShearedButton? NextButton => DisplayedFooterContent?.NextButton;
public ShearedButton BackButton = null!;
private readonly Bindable<bool> showFirstRunSetup = new Bindable<bool>(); private readonly Bindable<bool> showFirstRunSetup = new Bindable<bool>();
@ -90,7 +90,7 @@ namespace osu.Game.Overlays
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = 20, }, Padding = new MarginPadding { Bottom = 20 },
Child = new GridContainer Child = new GridContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -134,51 +134,6 @@ namespace osu.Game.Overlays
} }
}, },
}); });
FooterContent.Add(new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Vertical = PADDING },
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 10),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 10),
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new[]
{
Empty(),
BackButton = new ShearedButton(300)
{
Text = CommonStrings.Back,
Action = showPreviousStep,
Enabled = { Value = false },
DarkerColour = colours.Pink2,
LighterColour = colours.Pink1,
},
NextButton = new ShearedButton(0)
{
RelativeSizeAxes = Axes.X,
Width = 1,
Text = FirstRunSetupOverlayStrings.GetStarted,
DarkerColour = ColourProvider.Colour2,
LighterColour = ColourProvider.Colour1,
Action = showNextStep
},
Empty(),
},
}
});
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -190,6 +145,36 @@ namespace osu.Game.Overlays
if (showFirstRunSetup.Value) Show(); if (showFirstRunSetup.Value) Show();
} }
[Resolved]
private ScreenFooter footer { get; set; } = null!;
public new FirstRunSetupFooterContent? DisplayedFooterContent => base.DisplayedFooterContent as FirstRunSetupFooterContent;
public override VisibilityContainer CreateFooterContent()
{
var footerContent = new FirstRunSetupFooterContent
{
ShowNextStep = showNextStep,
};
footerContent.OnLoadComplete += _ => updateButtons();
return footerContent;
}
public override bool OnBackButton()
{
if (currentStepIndex == 0)
return false;
Debug.Assert(stack != null);
stack.CurrentScreen.Exit();
currentStepIndex--;
updateButtons();
return true;
}
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{ {
if (!e.Repeat) if (!e.Repeat)
@ -197,19 +182,12 @@ namespace osu.Game.Overlays
switch (e.Action) switch (e.Action)
{ {
case GlobalAction.Select: case GlobalAction.Select:
NextButton.TriggerClick(); DisplayedFooterContent?.NextButton.TriggerClick();
return true; return true;
case GlobalAction.Back: case GlobalAction.Back:
if (BackButton.Enabled.Value) footer.BackButton.TriggerClick();
{ return false;
BackButton.TriggerClick();
return true;
}
// If back button is disabled, we are at the first step.
// The base call will handle dismissal of the overlay.
break;
} }
} }
@ -279,19 +257,6 @@ namespace osu.Game.Overlays
showNextStep(); showNextStep();
} }
private void showPreviousStep()
{
if (currentStepIndex == 0)
return;
Debug.Assert(stack != null);
stack.CurrentScreen.Exit();
currentStepIndex--;
updateButtons();
}
private void showNextStep() private void showNextStep()
{ {
Debug.Assert(currentStepIndex != null); Debug.Assert(currentStepIndex != null);
@ -322,29 +287,61 @@ namespace osu.Game.Overlays
updateButtons(); updateButtons();
} }
private void updateButtons() private void updateButtons() => DisplayedFooterContent?.UpdateButtons(currentStepIndex, steps);
{
BackButton.Enabled.Value = currentStepIndex > 0;
NextButton.Enabled.Value = currentStepIndex != null;
if (currentStepIndex == null) public partial class FirstRunSetupFooterContent : VisibilityContainer
{
public ShearedButton NextButton { get; private set; } = null!;
public Action? ShowNextStep;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.Both;
InternalChild = NextButton = new ShearedButton(0)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Right = 12f },
RelativeSizeAxes = Axes.X,
Width = 1,
Text = FirstRunSetupOverlayStrings.GetStarted,
DarkerColour = colourProvider.Colour2,
LighterColour = colourProvider.Colour1,
Action = () => ShowNextStep?.Invoke(),
};
}
public void UpdateButtons(int? currentStep, IReadOnlyList<Type> steps)
{
NextButton.Enabled.Value = currentStep != null;
if (currentStep == null)
return; return;
bool isFirstStep = currentStepIndex == 0; bool isFirstStep = currentStep == 0;
bool isLastStep = currentStepIndex == steps.Count - 1; bool isLastStep = currentStep == steps.Count - 1;
if (isFirstStep) if (isFirstStep)
{
BackButton.Text = CommonStrings.Back;
NextButton.Text = FirstRunSetupOverlayStrings.GetStarted; NextButton.Text = FirstRunSetupOverlayStrings.GetStarted;
}
else else
{ {
BackButton.Text = LocalisableString.Interpolate($@"{CommonStrings.Back} ({steps[currentStepIndex.Value - 1].GetLocalisableDescription()})");
NextButton.Text = isLastStep NextButton.Text = isLastStep
? CommonStrings.Finish ? CommonStrings.Finish
: LocalisableString.Interpolate($@"{CommonStrings.Next} ({steps[currentStepIndex.Value + 1].GetLocalisableDescription()})"); : LocalisableString.Interpolate($@"{CommonStrings.Next} ({steps[currentStep.Value + 1].GetLocalisableDescription()})");
}
}
protected override void PopIn()
{
this.FadeIn();
}
protected override void PopOut()
{
this.Delay(400).FadeOut();
} }
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Diagnostics.CodeAnalysis;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -22,7 +23,7 @@ namespace osu.Game.Overlays
public virtual LocalisableString Title => Header.Title.Title; public virtual LocalisableString Title => Header.Title.Title;
public virtual LocalisableString Description => Header.Title.Description; public virtual LocalisableString Description => Header.Title.Description;
public T Header { get; } public T Header { get; private set; }
protected virtual Color4 BackgroundColour => ColourProvider.Background5; protected virtual Color4 BackgroundColour => ColourProvider.Background5;
@ -34,11 +35,12 @@ namespace osu.Game.Overlays
protected override Container<Drawable> Content => content; protected override Container<Drawable> Content => content;
private readonly Box background;
private readonly Container content; private readonly Container content;
protected FullscreenOverlay(OverlayColourScheme colourScheme) protected FullscreenOverlay(OverlayColourScheme colourScheme)
{ {
Header = CreateHeader(); RecreateHeader();
ColourProvider = new OverlayColourProvider(colourScheme); ColourProvider = new OverlayColourProvider(colourScheme);
@ -60,10 +62,9 @@ namespace osu.Game.Overlays
base.Content.AddRange(new Drawable[] base.Content.AddRange(new Drawable[]
{ {
new Box background = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = BackgroundColour
}, },
content = new Container content = new Container
{ {
@ -75,14 +76,17 @@ namespace osu.Game.Overlays
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Waves.FirstWaveColour = ColourProvider.Light4; UpdateColours();
Waves.SecondWaveColour = ColourProvider.Light3;
Waves.ThirdWaveColour = ColourProvider.Dark4;
Waves.FourthWaveColour = ColourProvider.Dark3;
} }
protected abstract T CreateHeader(); protected abstract T CreateHeader();
[MemberNotNull(nameof(Header))]
protected void RecreateHeader()
{
Header = CreateHeader();
}
public override void Show() public override void Show()
{ {
if (State.Value == Visibility.Visible) if (State.Value == Visibility.Visible)
@ -96,6 +100,18 @@ namespace osu.Game.Overlays
} }
} }
/// <summary>
/// Updates the colours of the background and the top waves with the latest colour shades provided by <see cref="ColourProvider"/>.
/// </summary>
protected void UpdateColours()
{
Waves.FirstWaveColour = ColourProvider.Light4;
Waves.SecondWaveColour = ColourProvider.Light3;
Waves.ThirdWaveColour = ColourProvider.Dark4;
Waves.FourthWaveColour = ColourProvider.Dark3;
background.Colour = BackgroundColour;
}
protected override void PopIn() protected override void PopIn()
{ {
base.PopIn(); base.PopIn();

View File

@ -157,6 +157,7 @@ namespace osu.Game.Overlays.Login
}, },
}; };
updateDropdownCurrent(status.Value);
dropdown.Current.BindValueChanged(action => dropdown.Current.BindValueChanged(action =>
{ {
switch (action.NewValue) switch (action.NewValue)

View File

@ -108,8 +108,6 @@ namespace osu.Game.Overlays.Mods
updateValues(); updateValues();
}, true); }, true);
BeatmapInfo.BindValueChanged(_ => updateValues());
Collapsed.BindValueChanged(_ => Collapsed.BindValueChanged(_ =>
{ {
// Only start autosize animations on first collapse toggle. This avoids an ugly initial presentation. // Only start autosize animations on first collapse toggle. This avoids an ugly initial presentation.
@ -120,12 +118,32 @@ namespace osu.Game.Overlays.Mods
GameRuleset = game.Ruleset.GetBoundCopy(); GameRuleset = game.Ruleset.GetBoundCopy();
GameRuleset.BindValueChanged(_ => updateValues()); GameRuleset.BindValueChanged(_ => updateValues());
BeatmapInfo.BindValueChanged(_ => updateValues()); BeatmapInfo.BindValueChanged(_ =>
{
updateStarDifficultyBindable();
updateValues(); updateValues();
}, true);
updateCollapsedState(); updateCollapsedState();
} }
private void updateStarDifficultyBindable()
{
cancellationSource?.Cancel();
if (BeatmapInfo.Value == null)
return;
starDifficulty = difficultyCache.GetBindableDifficulty(BeatmapInfo.Value, (cancellationSource = new CancellationTokenSource()).Token);
starDifficulty.BindValueChanged(s =>
{
starRatingDisplay.Current.Value = s.NewValue ?? default;
if (!starRatingDisplay.IsPresent)
starRatingDisplay.FinishTransforms(true);
});
}
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
startAnimating(); startAnimating();
@ -154,17 +172,6 @@ namespace osu.Game.Overlays.Mods
if (BeatmapInfo.Value == null) if (BeatmapInfo.Value == null)
return; return;
cancellationSource?.Cancel();
starDifficulty = difficultyCache.GetBindableDifficulty(BeatmapInfo.Value, (cancellationSource = new CancellationTokenSource()).Token);
starDifficulty.BindValueChanged(s =>
{
starRatingDisplay.Current.Value = s.NewValue ?? default;
if (!starRatingDisplay.IsPresent)
starRatingDisplay.FinishTransforms(true);
});
double rate = ModUtils.CalculateRateWithMods(Mods.Value); double rate = ModUtils.CalculateRateWithMods(Mods.Value);
bpmDisplay.Current.Value = FormatUtils.RoundBPM(BeatmapInfo.Value.BPM, rate); bpmDisplay.Current.Value = FormatUtils.RoundBPM(BeatmapInfo.Value.BPM, rate);

View File

@ -4,6 +4,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
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;
@ -11,14 +12,16 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK;
using osu.Game.Localisation; using osu.Game.Localisation;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public partial class ModCustomisationHeader : OsuHoverContainer public partial class ModCustomisationHeader : OsuHoverContainer
{ {
private Box background = null!; private Box background = null!;
private Box backgroundFlash = null!;
private SpriteIcon icon = null!; private SpriteIcon icon = null!;
[Resolved] [Resolved]
@ -46,6 +49,13 @@ namespace osu.Game.Overlays.Mods
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
backgroundFlash = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.White.Opacity(0.4f),
Blending = BlendingParameters.Additive,
Alpha = 0,
},
new OsuSpriteText new OsuSpriteText
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
@ -84,6 +94,12 @@ namespace osu.Game.Overlays.Mods
TooltipText = e.NewValue TooltipText = e.NewValue
? string.Empty ? string.Empty
: ModSelectOverlayStrings.CustomisationPanelDisabledReason; : ModSelectOverlayStrings.CustomisationPanelDisabledReason;
if (e.NewValue)
{
backgroundFlash.FadeInFromZero(150, Easing.OutQuad).Then()
.FadeOutFromOne(350, Easing.OutQuad);
}
}, true); }, true);
Expanded.BindValueChanged(v => Expanded.BindValueChanged(v =>

View File

@ -7,7 +7,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osuTK; using osuTK;
@ -19,7 +18,7 @@ namespace osu.Game.Overlays.Mods
private const double transition_duration = 200; private const double transition_duration = 200;
private readonly OsuSpriteText descriptionText; private readonly TextFlowContainer descriptionText;
public ModPresetTooltip(OverlayColourProvider colourProvider) public ModPresetTooltip(OverlayColourProvider colourProvider)
{ {
@ -44,11 +43,15 @@ namespace osu.Game.Overlays.Mods
Spacing = new Vector2(7), Spacing = new Vector2(7),
Children = new[] Children = new[]
{ {
descriptionText = new OsuSpriteText descriptionText = new TextFlowContainer(f =>
{ {
Font = OsuFont.GetFont(weight: FontWeight.Regular), f.Font = OsuFont.GetFont(weight: FontWeight.Regular);
Colour = colourProvider.Content1, f.Colour = colourProvider.Content1;
}, })
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
} }
} }
}; };

View File

@ -138,6 +138,7 @@ namespace osu.Game.Overlays.Mods
}, },
new GridContainer new GridContainer
{ {
Padding = new MarginPadding { Top = 1, Bottom = 3 },
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
RowDimensions = new[] RowDimensions = new[]
{ {

View File

@ -0,0 +1,177 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Mods;
using osuTK;
namespace osu.Game.Overlays.Mods
{
public partial class ModSelectFooterContent : VisibilityContainer
{
private readonly ModSelectOverlay overlay;
private RankingInformationDisplay? rankingInformationDisplay;
private BeatmapAttributesDisplay? beatmapAttributesDisplay;
private FillFlowContainer<ShearedButton> buttonFlow = null!;
private FillFlowContainer contentFlow = null!;
public DeselectAllModsButton? DeselectAllModsButton { get; set; }
public readonly IBindable<WorkingBeatmap?> Beatmap = new Bindable<WorkingBeatmap?>();
public readonly IBindable<IReadOnlyList<Mod>> ActiveMods = new Bindable<IReadOnlyList<Mod>>();
/// <summary>
/// Whether the effects (on score multiplier, on or beatmap difficulty) of the current selected set of mods should be shown.
/// </summary>
protected virtual bool ShowModEffects => true;
/// <summary>
/// Whether the ranking information and beatmap attributes displays are stacked vertically due to small space.
/// </summary>
public bool DisplaysStackedVertically { get; private set; }
public ModSelectFooterContent(ModSelectOverlay overlay)
{
this.overlay = overlay;
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.Both;
InternalChild = buttonFlow = new FillFlowContainer<ShearedButton>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Padding = new MarginPadding { Horizontal = 20 },
Spacing = new Vector2(10),
ChildrenEnumerable = CreateButtons(),
};
if (ShowModEffects)
{
AddInternal(contentFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(30, 10),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding { Horizontal = 20 },
Children = new Drawable[]
{
rankingInformationDisplay = new RankingInformationDisplay
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight
},
beatmapAttributesDisplay = new BeatmapAttributesDisplay
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
BeatmapInfo = { Value = Beatmap.Value?.BeatmapInfo },
},
}
});
}
}
private ModSettingChangeTracker? modSettingChangeTracker;
protected override void LoadComplete()
{
base.LoadComplete();
Beatmap.BindValueChanged(b =>
{
if (beatmapAttributesDisplay != null)
beatmapAttributesDisplay.BeatmapInfo.Value = b.NewValue?.BeatmapInfo;
}, true);
ActiveMods.BindValueChanged(m =>
{
updateInformation();
modSettingChangeTracker?.Dispose();
// Importantly, use ActiveMods.Value here (and not the ValueChanged NewValue) as the latter can
// potentially be stale, due to complexities in the way change trackers work.
//
// See https://github.com/ppy/osu/pull/23284#issuecomment-1529056988
modSettingChangeTracker = new ModSettingChangeTracker(ActiveMods.Value);
modSettingChangeTracker.SettingChanged += _ => updateInformation();
}, true);
}
private void updateInformation()
{
if (rankingInformationDisplay != null)
{
double multiplier = 1.0;
foreach (var mod in ActiveMods.Value)
multiplier *= mod.ScoreMultiplier;
rankingInformationDisplay.ModMultiplier.Value = multiplier;
rankingInformationDisplay.Ranked.Value = ActiveMods.Value.All(m => m.Ranked);
}
if (beatmapAttributesDisplay != null)
beatmapAttributesDisplay.Mods.Value = ActiveMods.Value;
}
protected override void Update()
{
base.Update();
if (beatmapAttributesDisplay != null)
{
float rightEdgeOfLastButton = buttonFlow[^1].ScreenSpaceDrawQuad.TopRight.X;
// this is cheating a bit; the 640 value is hardcoded based on how wide the expanded panel _generally_ is.
// due to the transition applied, the raw screenspace quad of the panel cannot be used, as it will trigger an ugly feedback cycle of expanding and collapsing.
float projectedLeftEdgeOfExpandedBeatmapAttributesDisplay = buttonFlow.ToScreenSpace(buttonFlow.DrawSize - new Vector2(640, 0)).X;
DisplaysStackedVertically = rightEdgeOfLastButton > projectedLeftEdgeOfExpandedBeatmapAttributesDisplay;
// only update preview panel's collapsed state after we are fully visible, to ensure all the buttons are where we expect them to be.
if (Alpha == 1)
beatmapAttributesDisplay.Collapsed.Value = DisplaysStackedVertically;
contentFlow.LayoutDuration = 200;
contentFlow.LayoutEasing = Easing.OutQuint;
contentFlow.Direction = DisplaysStackedVertically ? FillDirection.Vertical : FillDirection.Horizontal;
}
}
protected virtual IEnumerable<ShearedButton> CreateButtons() => new[]
{
DeselectAllModsButton = new DeselectAllModsButton(overlay)
};
protected override void PopIn()
{
this.MoveToY(0, 400, Easing.OutQuint)
.FadeIn(400, Easing.OutQuint);
}
protected override void PopOut()
{
this.MoveToY(-20f, 200, Easing.OutQuint)
.FadeOut(200, Easing.OutQuint);
}
}
}

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