1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 08:27:49 +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": {
"version": "2024.517.0",
"version": "2024.802.0",
"commands": [
"localisation"
]

View File

@ -64,10 +64,11 @@ jobs:
matrix:
os:
- { 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 }
threadingMode: ['SingleThread', 'MultiThreaded']
timeout-minutes: 60
timeout-minutes: 120
steps:
- name: Checkout
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.
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.

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.702.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.802.0" />
</ItemGroup>
<PropertyGroup>
<!-- 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);
addVertexCheckStep(3, 1, times[0], positions[0]);
addDragEndStep();
}
[Test]
@ -100,6 +101,9 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddMouseMoveStep(times[2] - 50, positions[2] - 50);
addVertexCheckStep(4, 1, times[1] - 50, positions[1] - 50);
addVertexCheckStep(4, 2, times[2] - 50, positions[2] - 50);
AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft));
addDragEndStep();
}
[Test]
@ -113,6 +117,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addDragStartStep(times[1], positions[1]);
AddMouseMoveStep(times[1], 400);
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityMultiplierBindable.IsDefault);
addDragEndStep();
}
[Test]
@ -129,6 +134,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddStep("scroll playfield", () => manualClock.CurrentTime += 200);
AddMouseMoveStep(times[1] + 200, positions[1] + 100);
addVertexCheckStep(2, 1, times[1] + 200, positions[1] + 100);
addDragEndStep();
}
[Test]
@ -161,18 +167,18 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addAddVertexSteps(500, 150);
addVertexCheckStep(3, 1, 500, 150);
addAddVertexSteps(90, 200);
addVertexCheckStep(4, 1, times[0], positions[0]);
addAddVertexSteps(160, 200);
addVertexCheckStep(4, 1, 160, 200);
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));
}
[Test]
public void TestDeleteVertex()
{
double[] times = { 100, 300, 500 };
double[] times = { 100, 300, 400 };
float[] positions = { 100, 200, 150 };
addBlueprintStep(times, positions);
@ -265,7 +271,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddStep("delete vertex", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Click(MouseButton.Left);
InputManager.Click(MouseButton.Right);
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.Objects.Types;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
@ -42,6 +43,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
[Resolved]
private IBeatSnapProvider? beatSnapProvider { get; set; }
[Resolved]
protected EditorBeatmap? EditorBeatmap { get; private set; }
protected EditablePath(Func<float, double> 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.
// In case the required velocity is too large, the path is not preserved.
double previousVelocity = svBindable.Value;
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;
double endTime = hitObject.StartTime + path.Duration;
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)

View File

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

View File

@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osuTK;
@ -12,6 +13,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public partial class VertexPiece : Circle
{
private VertexState state = new VertexState();
[Resolved]
private OsuColour osuColour { get; set; } = null!;
@ -24,7 +27,32 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
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;
}
}

View File

@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
scrollingPath = new ScrollingPath(),
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
{
public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation
public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation, IHasTimePreempt
{
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 Type[] IncompatibleMods => new[] { typeof(ManiaModInvert) };
public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert), typeof(ManiaModNoRelease) };
public void ApplyToBeatmap(IBeatmap beatmap)
{

View File

@ -1,6 +1,7 @@
// 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 System.Threading;
using osu.Framework.Localisation;
@ -27,6 +28,8 @@ namespace osu.Game.Rulesets.Mania.Mods
public override ModType Type => ModType.DifficultyReduction;
public override Type[] IncompatibleMods => new[] { typeof(ManiaModHoldOff) };
public void ApplyToBeatmap(IBeatmap beatmap)
{
var maniaBeatmap = (ManiaBeatmap)beatmap;

View File

@ -268,11 +268,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
ApplyMaxResult();
else
MissForcefully();
}
// 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);
// Important that this is always called when a result is applied.
endHold();
}
}
public override void MissForcefully()

View File

@ -42,7 +42,12 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{
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)
{
@ -50,14 +55,14 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
foreach (var h in hitObjects)
h.StackHeight = 0;
if (Beatmap.BeatmapInfo.BeatmapVersion >= 6)
applyStacking(Beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1);
if (beatmap.BeatmapInfo.BeatmapVersion >= 6)
applyStacking(beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1);
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.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++)
{

View File

@ -295,6 +295,12 @@ namespace osu.Game.Rulesets.Osu.Edit
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
snapResult = new SnapResult(closestSnapPosition, null, playfield);
return true;

View File

@ -13,6 +13,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components;
@ -50,12 +51,33 @@ namespace osu.Game.Rulesets.Osu.Edit
{
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...
foreach (var h in hitObjects)
h.Position += this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta);
h.Position += localDelta;
// but this will be corrected.
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;
}

View File

@ -5,19 +5,23 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class DrawableOsuJudgement : DrawableJudgement
{
internal Color4 AccentColour { get; private set; }
internal SkinnableLighting Lighting { get; private set; } = null!;
[Resolved]
private OsuConfigManager config { get; set; } = null!;
private bool positionTransferred;
private Vector2 screenSpacePosition;
[BackgroundDependencyLoader]
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()
{
base.PrepareForUse();
Lighting.ResetAnimation();
Lighting.SetColourFrom(JudgedObject, Result);
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);
}
Lighting.SetColourFrom(this, Result);
Position = Parent!.ToLocalSpace(screenSpacePosition);
}
protected override void ApplyHitAnimations()

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
@ -12,8 +10,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
internal partial class SkinnableLighting : SkinnableSprite
{
private DrawableHitObject targetObject;
private JudgementResult targetResult;
private DrawableOsuJudgement? targetJudgement;
private JudgementResult? targetResult;
public SkinnableLighting()
: base("lighting")
@ -29,11 +27,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// <summary>
/// Updates the lighting colour from a given hitobject and result.
/// </summary>
/// <param name="targetObject">The <see cref="DrawableHitObject"/> that's been judged.</param>
/// <param name="targetResult">The <see cref="JudgementResult"/> that <paramref name="targetObject"/> was judged with.</param>
public void SetColourFrom(DrawableHitObject targetObject, JudgementResult targetResult)
/// <param name="targetJudgement">The <see cref="DrawableHitObject"/> that's been judged.</param>
/// <param name="targetResult">The <see cref="JudgementResult"/> that <paramref name="targetJudgement"/> was judged with.</param>
public void SetColourFrom(DrawableOsuJudgement targetJudgement, JudgementResult? targetResult)
{
this.targetObject = targetObject;
this.targetJudgement = targetJudgement;
this.targetResult = targetResult;
updateColour();
@ -41,10 +39,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private void updateColour()
{
if (targetObject == null || targetResult == null)
if (targetJudgement == null || targetResult == null)
Colour = Color4.White;
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
{
public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition
public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition, IHasTimePreempt
{
/// <summary>
/// The radius of hit objects (ie. the radius of a <see cref="HitCircle"/>).
@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary>
public const double PREEMPT_MAX = 1800;
public double TimePreempt = 600;
public double TimePreempt { get; set; } = 600;
public double TimeFadeIn = 400;
private HitObjectProperty<Vector2> position;

View File

@ -16,7 +16,6 @@ using osu.Framework.Graphics.Shaders.Types;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Framework.Timing;
using osuTK;
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
parts[i].InvalidationID = -1;
}
AddLayout(partSizeCache);
}
[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>
/// The amount of time to fade the cursor trail pieces.
/// </summary>
@ -156,6 +147,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
protected void AddTrail(Vector2 position)
{
position = ToLocalSpace(position);
if (InterpolateMovements)
{
if (!lastPosition.HasValue)
@ -174,7 +167,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
float distance = diff.Length;
Vector2 direction = diff / distance;
float interval = partSize.X / 2.5f * IntervalMultiplier;
float interval = Texture.DisplayWidth / 2.5f * IntervalMultiplier;
float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0);
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].InvalidationID;
@ -220,7 +213,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private float fadeExponent;
private readonly TrailPart[] parts = new TrailPart[max_sprites];
private Vector2 size;
private Vector2 originPosition;
private IVertexBatch<TexturedTrailVertex> vertexBatch;
@ -236,7 +228,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
shader = Source.shader;
texture = Source.texture;
size = Source.partSize;
time = Source.time;
fadeExponent = Source.FadeExponent;
@ -277,6 +268,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
RectangleF textureRect = texture.GetTextureRect();
renderer.PushLocalMatrix(DrawInfo.Matrix);
foreach (var part in parts)
{
if (part.InvalidationID == -1)
@ -287,7 +280,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
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,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
@ -296,7 +289,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
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,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomRight.Linear,
@ -305,7 +298,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
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,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopRight.Linear,
@ -314,7 +307,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
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,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopLeft.Linear,
@ -322,6 +315,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
});
}
renderer.PopLocalMatrix();
vertexBatch.Draw();
shader.Unbind();
}

View File

@ -206,6 +206,15 @@ namespace osu.Game.Rulesets.Osu.UI
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
{
public void Add(Drawable proxy) => AddInternal(proxy);

View File

@ -33,9 +33,30 @@ namespace osu.Game.Rulesets.Osu.UI
[BackgroundDependencyLoader]
private void load()
{
OsuResumeOverlayInputBlocker? inputBlocker = null;
if (drawableRuleset != null)
{
var osuPlayfield = (OsuPlayfield)drawableRuleset.Playfield;
osuPlayfield.AttachResumeOverlayInputBlocker(inputBlocker = new OsuResumeOverlayInputBlocker());
}
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;
scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
ResumeRequested?.Invoke();
return true;
}
@ -141,5 +161,27 @@ namespace osu.Game.Rulesets.Osu.UI
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>
<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" />
</ItemGroup>
</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);
});
// You might think that this should be a SwellTick since we're before the swell, but SwellTicks get no StartTime (ie. they are zero).
// 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>);
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, 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);
});
// You might think that this should be a SwellTick since we're before the swell, but SwellTicks get no StartTime (ie. they are zero).
// 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>);
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);

View File

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

View File

@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
cancellationToken.ThrowIfCancellationRequested();
AddNested(new SwellTick
{
StartTime = StartTime,
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);
}
[Test]
public void TestScoreUpgradeFailed()
[TestCase(30000002)]
[TestCase(30000013)]
public void TestScoreUpgradeFailed(int scoreVersion)
{
ScoreInfo scoreInfo = null!;
@ -172,16 +173,18 @@ namespace osu.Game.Tests.Database
Ruleset = r.All<RulesetInfo>().First(),
})
{
TotalScoreVersion = 30000002,
TotalScoreVersion = scoreVersion,
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);
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]

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]
public void TestNoChanges()
{
@ -272,21 +310,25 @@ namespace osu.Game.Tests.Database
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
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);
realm.Run(r => r.Refresh());
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));
});
}
@ -479,6 +521,7 @@ namespace osu.Game.Tests.Database
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory =>
{
// arbitrary beatmap removal
@ -496,7 +539,7 @@ namespace osu.Game.Tests.Database
Debug.Assert(importAfterUpdate != null);
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: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) - 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))]

View File

@ -45,7 +45,7 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new Note { StartTime = 1000 },
}
});
@ -67,8 +67,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 2000 },
new Note { StartTime = 1000 },
new Note { StartTime = 2000 },
}
});
@ -136,8 +136,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 5000 },
new Note { StartTime = 1000 },
new Note { StartTime = 5000 },
}
});
@ -164,8 +164,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 9000 },
new Note { StartTime = 1000 },
new Note { StartTime = 9000 },
},
Breaks =
{
@ -197,9 +197,9 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 5000 },
new HitCircle { StartTime = 9000 },
new Note { StartTime = 1000 },
new Note { StartTime = 5000 },
new Note { StartTime = 9000 },
},
Breaks =
{
@ -232,8 +232,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1100 },
new HitCircle { StartTime = 9000 },
new Note { StartTime = 1100 },
new Note { StartTime = 9000 },
},
Breaks =
{
@ -264,8 +264,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 9000 },
new Note { StartTime = 1000 },
new Note { StartTime = 9000 },
},
Breaks =
{
@ -299,9 +299,9 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 5000 },
new HitCircle { StartTime = 9000 },
new Note { StartTime = 1000 },
new Note { StartTime = 5000 },
new Note { StartTime = 9000 },
},
Breaks =
{
@ -334,8 +334,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 9000 },
new Note { StartTime = 1000 },
new Note { StartTime = 9000 },
},
Breaks =
{
@ -366,8 +366,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 2000 },
new Note { StartTime = 1000 },
new Note { StartTime = 2000 },
},
Breaks =
{
@ -393,8 +393,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 2000 },
new Note { StartTime = 1000 },
new Note { StartTime = 2000 },
},
Breaks =
{
@ -447,8 +447,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 10000 },
new HitCircle { StartTime = 11000 },
new Note { StartTime = 10000 },
new Note { StartTime = 11000 },
},
Breaks =
{
@ -474,8 +474,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 10000 },
new HitCircle { StartTime = 11000 },
new Note { StartTime = 10000 },
new Note { StartTime = 11000 },
},
Breaks =
{
@ -489,5 +489,55 @@ namespace osu.Game.Tests.Editing
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.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Tests.Mods
@ -105,9 +107,6 @@ namespace osu.Game.Tests.Mods
testMod.ResetSettingsToDefaults();
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);
var applied = applyDifficulty(new BeatmapDifficulty
@ -119,6 +118,48 @@ namespace osu.Game.Tests.Mods
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>
/// 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.

View File

@ -4,16 +4,34 @@
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Online.API;
using osu.Game.Online.Metadata;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.Metadata;
using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.DailyChallenge
{
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]
public void TestDailyChallenge()
{
@ -36,5 +54,33 @@ namespace osu.Game.Tests.Visual.DailyChallenge
AddStep("add room", () => API.Perform(new CreateRoomRequest(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.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
@ -17,14 +18,14 @@ namespace osu.Game.Tests.Visual.DailyChallenge
{
public partial class TestSceneDailyChallengeEventFeed : OsuTestScene
{
private DailyChallengeEventFeed feed = null!;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
[Test]
public void TestBasicAppearance()
[SetUpSteps]
public void SetUpSteps()
{
DailyChallengeEventFeed feed = null!;
AddStep("create content", () => Children = new Drawable[]
{
new Box
@ -35,22 +36,28 @@ namespace osu.Game.Tests.Visual.DailyChallenge
feed = new DailyChallengeEventFeed
{
RelativeSizeAxes = Axes.Both,
Height = 0.3f,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
});
AddSliderStep("adjust width", 0.1f, 1, 1, width =>
{
if (feed.IsNotNull())
feed.Width = width;
});
AddSliderStep("adjust height", 0.1f, 1, 1, height =>
AddSliderStep("adjust height", 0.1f, 1, 0.3f, height =>
{
if (feed.IsNotNull())
feed.Height = height;
});
}
AddStep("add normal score", () =>
[Test]
public void TestBasicAppearance()
{
AddRepeatStep("add normal score", () =>
{
var ev = new NewScoreEvent(1, new APIUser
{
@ -60,9 +67,9 @@ namespace osu.Game.Tests.Visual.DailyChallenge
}, RNG.Next(1_000_000), null);
feed.AddNewScore(ev);
});
}, 50);
AddStep("add new user best", () =>
AddRepeatStep("add new user best", () =>
{
var ev = new NewScoreEvent(1, new APIUser
{
@ -75,9 +82,9 @@ namespace osu.Game.Tests.Visual.DailyChallenge
testScore.TotalScore = RNG.Next(1_000_000);
feed.AddNewScore(ev);
});
}, 50);
AddStep("add top 10 score", () =>
AddRepeatStep("add top 10 score", () =>
{
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));
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.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
@ -19,11 +21,11 @@ namespace osu.Game.Tests.Visual.DailyChallenge
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
[Test]
public void TestBasicAppearance()
{
DailyChallengeScoreBreakdown breakdown = null!;
private DailyChallengeScoreBreakdown breakdown = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create content", () => Children = new Drawable[]
{
new Box
@ -49,7 +51,14 @@ namespace osu.Game.Tests.Visual.DailyChallenge
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]));
}
[Test]
public void TestBasicAppearance()
{
AddStep("add new score", () =>
{
var ev = new NewScoreEvent(1, new APIUser
@ -61,6 +70,27 @@ namespace osu.Game.Tests.Visual.DailyChallenge
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())
ring.Height = height;
});
AddToggleStep("toggle visible", v => ring.Alpha = v ? 1 : 0);
AddStep("just started", () =>
{
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));
}
[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]
public void TestHotkeysAffectNodeSamples()
{

View File

@ -28,6 +28,74 @@ namespace osu.Game.Tests.Visual.Editing
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]
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);
AddAssert("preview time line should not show", () => !Editor.ChildrenOfType<PreviewTimePart>().Single().Children.Any());
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);
});
AddUntilStep("wait for sequence", () => !Scheduler.HasPendingTasks);
}
[Test]
@ -120,6 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}
}
});
AddUntilStep("wait for sequence", () => !Scheduler.HasPendingTasks);
}
[Test]

View File

@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Gameplay
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.
// 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);
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]

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@ -34,7 +32,7 @@ namespace osu.Game.Tests.Visual.Menus
[Test]
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.
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);
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
AddAssert("track changed to previous", () =>
AddUntilStep("track changed to previous", () =>
trackChangeQueue.Count == 1 &&
trackChangeQueue.Dequeue().changeDirection == TrackChangeDirection.Prev);
AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext));
AddAssert("track changed to next", () =>
AddUntilStep("track changed to next", () =>
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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Platform;
using osu.Framework.Testing;
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);
AddStep("move mouse to first item title", () =>
{
var drawQuad = playlist.ChildrenOfType<LinkFlowContainer>().First().ScreenSpaceDrawQuad;
var location = (drawQuad.TopLeft + drawQuad.BottomLeft) / 2 + new Vector2(drawQuad.Width * 0.2f, 0);
InputManager.MoveMouseTo(location);
});
AddStep("move mouse to first item title", () => InputManager.MoveMouseTo(playlist.ChildrenOfType<LinkFlowContainer>().First().ChildrenOfType<SpriteText>().First()));
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);
// implies being clickable.
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)));

View File

@ -3,6 +3,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@ -16,6 +17,7 @@ using osu.Framework.Testing;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Footer;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Utils;
using osuTK.Input;
@ -26,6 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
private FreeModSelectOverlay freeModSelectOverlay;
private FooterButtonFreeMods footerButtonFreeMods;
private ScreenFooter footer;
private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
[BackgroundDependencyLoader]
@ -127,7 +130,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
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"));
AddStep("click footer select all button", () =>
@ -150,7 +153,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
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
{
@ -160,9 +166,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Y = -ScreenFooter.HEIGHT,
Current = { BindTarget = freeModSelectOverlay.SelectedMods },
},
footer = new ScreenFooter(),
},
CachedDependencies = new (Type, object)[] { (typeof(ScreenFooter), footer) },
});
AddUntilStep("all column content loaded",
() => freeModSelectOverlay.ChildrenOfType<ModColumn>().Any()
&& 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);
ClickButtonWhenEnabled<UserModSelectButton>();
AddAssert("mod select shows unranked", () => screen.UserModsSelectOverlay.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("mod select shows unranked", () => this.ChildrenOfType<RankingInformationDisplay>().Single().Ranked.Value == false);
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());
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);
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

View File

@ -1,12 +1,16 @@
// 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.IO;
using System.Linq;
using System.Threading;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Screens;
@ -204,12 +208,16 @@ namespace osu.Game.Tests.Visual.Navigation
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 editor load start", () => loader.Editor != null);
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);
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());
}
@ -356,5 +364,33 @@ namespace osu.Game.Tests.Visual.Navigation
private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType<EditorBeatmap>().Single();
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]
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", () =>
{
Game.Notifications.Post(new ProgressNotification
{
Text = "Something is still running",
Text = $"Something is still running #{++x}",
Progress = 0.5f,
State = ProgressNotificationState.Active,
});
}, 15);
AddAssert("all notifications = 15", () => Game.Notifications.AllNotifications.Count(), () => Is.EqualTo(15));
AddStep("Hold escape", () => InputManager.PressKey(Key.Escape));
AddUntilStep("confirmation dialog shown", () => Game.ChildrenOfType<DialogOverlay>().Single().CurrentDialog is ConfirmExitDialog);
AddStep("Release escape", () => InputManager.ReleaseKey(Key.Escape));
@ -945,6 +952,8 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestTouchScreenDetectionAtSongSelect()
{
AddUntilStep("wait for settings", () => Game.Settings.IsLoaded);
AddStep("touch logo", () =>
{
var button = Game.ChildrenOfType<OsuLogo>().Single();

View File

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

View File

@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays.Chat;
@ -40,6 +41,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 3,
Username = "LocalUser"
};
string uuid = Guid.NewGuid().ToString();
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);
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.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Online.API;
@ -24,7 +25,17 @@ namespace osu.Game.Tests.Visual.Online
[SetUpSteps]
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]
@ -111,6 +122,90 @@ namespace osu.Game.Tests.Visual.Online
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
{
Username = @"Somebody",
@ -201,6 +296,15 @@ namespace osu.Game.Tests.Visual.Online
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",
Colour = "ff0000",
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);
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);
}
}
@ -156,7 +156,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
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);
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);
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);
}
}

View File

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

View File

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

View File

@ -2,6 +2,7 @@
// 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.Graphics;
@ -10,6 +11,7 @@ using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Metadata;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.Menu;
using osuTK.Input;
using Color4 = osuTK.Graphics.Color4;
@ -39,8 +41,6 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestDailyChallengeButton()
{
AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null));
AddStep("set up API", () => dummyAPI.HandleRequest = 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)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
ButtonSystemState = ButtonSystemState.TopLevel,
});
NotificationOverlay notificationOverlay = null!;
DependencyProvidingContainer buttonContainer = null!;
AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo
{
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]
public void TestOutOfRangeValueStillApplied()
public void TestValueAboveRangeStillApplied()
{
AddStep("set override cs to 11", () => modDifficultyAdjust.CircleSize.Value = 11);
@ -91,6 +91,28 @@ namespace osu.Game.Tests.Visual.UserInterface
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]
public void TestExtendedLimits()
{
@ -109,6 +131,11 @@ namespace osu.Game.Tests.Visual.UserInterface
checkSliderAtValue("Circle Size", 11);
checkBindableAtValue("Circle Size", 11);
setSliderValue("Approach Rate", -5);
checkSliderAtValue("Approach Rate", -5);
checkBindableAtValue("Approach Rate", -5);
setExtendedLimits(false);
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.Mods;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Screens.Footer;
using osu.Game.Tests.Mods;
using osuTK;
using osuTK.Input;
@ -94,12 +95,28 @@ namespace osu.Game.Tests.Visual.UserInterface
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,
State = { Value = Visibility.Visible },
Beatmap = Beatmap.Value,
SelectedMods = { BindTarget = SelectedMods }
Beatmap = { Value = Beatmap.Value },
SelectedMods = { BindTarget = SelectedMods },
},
footer,
}
};
});
waitForColumnLoad();
}
@ -120,7 +137,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("mod multiplier correct", () =>
{
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
return Precision.AlmostEquals(multiplier, this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
});
assertCustomisationToggleState(disabled: false, active: false);
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
@ -135,7 +152,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("mod multiplier correct", () =>
{
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
return Precision.AlmostEquals(multiplier, this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
});
assertCustomisationToggleState(disabled: false, active: false);
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
@ -757,7 +774,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("click back button", () =>
{
InputManager.MoveMouseTo(modSelectOverlay.BackButton);
InputManager.MoveMouseTo(this.ChildrenOfType<ScreenBackButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
@ -885,7 +902,7 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.Click(MouseButton.Left);
});
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,
// 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()
.ChildrenOfType<RevertToDefaultButton<double>>().Single().TriggerClick());
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]
@ -1015,8 +1032,6 @@ namespace osu.Game.Tests.Visual.UserInterface
private partial class TestModSelectOverlay : UserModSelectOverlay
{
protected override bool ShowPresets => true;
public new ShearedButton BackButton => base.BackButton;
}
private class TestUnimplementedMod : Mod

View File

@ -76,6 +76,24 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.Click(MouseButton.Left);
});
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));

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 ScreenFooter screenFooter = null!;
private TestModSelectOverlay overlay = null!;
private TestModSelectOverlay modOverlay = null!;
[SetUp]
public void SetUp() => Schedule(() =>
@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.UserInterface
},
Children = new Drawable[]
{
overlay = new TestModSelectOverlay(),
modOverlay = new TestModSelectOverlay(),
new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.UserInterface
screenFooter.SetButtons(new ScreenFooterButton[]
{
new ScreenFooterButtonMods(overlay) { Current = SelectedMods },
new ScreenFooterButtonMods(modOverlay) { Current = SelectedMods },
new ScreenFooterButtonRandom(),
new ScreenFooterButtonOptions(),
});
@ -178,6 +178,24 @@ namespace osu.Game.Tests.Visual.UserInterface
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
{
protected override bool ShowPresets => true;
@ -185,8 +203,6 @@ namespace osu.Game.Tests.Visual.UserInterface
private partial class TestShearedOverlayContainer : ShearedOverlayContainer
{
public override bool UseNewFooter => true;
public TestShearedOverlayContainer()
: base(OverlayColourScheme.Orange)
{
@ -212,7 +228,7 @@ namespace osu.Game.Tests.Visual.UserInterface
return false;
}
public override Drawable CreateFooterContent() => new TestFooterContent();
public override VisibilityContainer CreateFooterContent() => new TestFooterContent();
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)
{
var originalDateAdded = original.DateAdded;
Guid originalId = original.ID;
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 (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.
ProcessBeatmap?.Invoke(s, MetadataLookupScope.OnlineFirst);
});
@ -79,7 +84,7 @@ namespace osu.Game.Beatmaps
original.DeletePending = true;
// Transfer local values which should be persisted across a beatmap update.
updated.DateAdded = original.DateAdded;
updated.DateAdded = originalDateAdded;
transferCollectionReferences(realm, original, updated);
@ -278,6 +283,9 @@ namespace osu.Game.Beatmaps
protected override void UndeleteForReuse(BeatmapSetInfo existing)
{
if (!existing.DeletePending)
return;
base.UndeleteForReuse(existing);
existing.DateAdded = DateTimeOffset.UtcNow;
}

View File

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

View File

@ -26,7 +26,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary>
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
{

View File

@ -101,7 +101,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
bool shouldDim = Dimmed.Value || playButton.Playing.Value;
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).
ShowDragHandle.Value = false;
Masking = true;
CornerRadius = item_height / 2;
}
protected override Drawable CreateContent() => new ItemContent(Model);
@ -50,7 +53,7 @@ namespace osu.Game.Collections
/// <summary>
/// The main content of the <see cref="DrawableCollectionListItem"/>.
/// </summary>
private partial class ItemContent : CircularContainer
private partial class ItemContent : CompositeDrawable
{
private readonly Live<BeatmapCollection> collection;
@ -65,13 +68,12 @@ namespace osu.Game.Collections
RelativeSizeAxes = Axes.X;
Height = item_height;
Masking = true;
}
[BackgroundDependencyLoader]
private void load()
{
Children = new[]
InternalChildren = new[]
{
collection.IsManaged
? 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!;
@ -155,7 +157,7 @@ namespace osu.Game.Collections
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
InternalChild = fadeContainer = new Container
Child = fadeContainer = new Container
{
RelativeSizeAxes = Axes.Both,
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);
@ -195,12 +205,7 @@ namespace osu.Game.Collections
{
background.FlashColour(Color4.White, 150);
if (collection.PerformRead(c => c.BeatmapMD5Hashes.Count) == 0)
deleteCollection();
else
dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection));
return true;
return base.OnClick(e);
}
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>(
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()
// must be done after materialisation, as realm doesn't support
// filtering on nested property predicates or projection via `.Select()`

View File

@ -64,6 +64,7 @@ namespace osu.Game.Graphics.Containers
{
InternalChildren = new Drawable[]
{
new HoverClickSounds(),
new GridContainer
{
RelativeSizeAxes = Axes.X,
@ -92,7 +93,6 @@ namespace osu.Game.Graphics.Containers
ColumnDimensions = 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.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Graphics.Cursor
{
@ -27,15 +27,20 @@ namespace osu.Game.Graphics.Cursor
public partial class OsuTooltip : Tooltip
{
private const float max_width = 500;
private readonly Box background;
private readonly OsuSpriteText text;
private readonly TextFlowContainer text;
private bool instantMovement = true;
public override void SetContent(LocalisableString contentString)
{
if (contentString == text.Text) return;
private LocalisableString lastContent;
text.Text = contentString;
public override void SetContent(LocalisableString content)
{
if (content.Equals(lastContent))
return;
text.Text = content;
if (IsPresent)
{
@ -44,6 +49,8 @@ namespace osu.Game.Graphics.Cursor
}
else
AutoSizeDuration = 0;
lastContent = content;
}
public OsuTooltip()
@ -65,10 +72,14 @@ namespace osu.Game.Graphics.Cursor
RelativeSizeAxes = Axes.Both,
Alpha = 0.9f,
},
text = new OsuSpriteText
text = new TextFlowContainer(f =>
{
Padding = new MarginPadding(5),
Font = OsuFont.GetFont(weight: FontWeight.Regular)
f.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")]
ButtonSidebar,
[Description("toolbar")]
Toolbar,
[Description("tabselect")]
TabSelect,
[Description("scrolltotop")]
ScrollToTop,
[Description("dialog-cancel")]
DialogCancel,

View File

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

View File

@ -261,7 +261,8 @@ namespace osu.Game.Graphics.UserInterface
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();
}

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.NewValue.Activity.BindTo(activity);
if (u.OldValue != null)
localUserStatus.UnbindFrom(u.OldValue.Status);
localUserStatus.BindTo(u.NewValue.Status);
u.OldValue?.Status.UnbindFrom(localUserStatus);
u.NewValue.Status.BindTo(localUserStatus);
}, true);
localUserStatus.BindValueChanged(val => configStatus.Value = val.NewValue);
localUserStatus.BindTo(configStatus);
var thread = new Thread(run)
{
@ -164,6 +163,8 @@ namespace osu.Game.Online.API
public string AccessToken => authentication.RequestAccessToken();
public Guid SessionIdentifier { get; } = Guid.NewGuid();
/// <summary>
/// Number of consecutive requests which failed due to network issues.
/// </summary>
@ -598,6 +599,7 @@ namespace osu.Game.Online.API
password = null;
SecondFactorCode = null;
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
Schedule(() =>

View File

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

View File

@ -44,6 +44,12 @@ namespace osu.Game.Online.API
/// </summary>
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>
/// Returns whether the local user is logged in.
/// </summary>

View File

@ -201,6 +201,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"playmode")]
public string PlayMode;
[JsonProperty(@"profile_hue")]
public int? ProfileHue;
[JsonProperty(@"profile_order")]
public string[] ProfileOrder;
@ -269,6 +272,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty("groups")]
public APIUserGroup[] Groups;
[JsonProperty("daily_challenge_user_stats")]
public APIUserDailyChallengeStatistics DailyChallengeStatistics = new APIUserDailyChallengeStatistics();
public override string ToString() => Username;
/// <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 override float TextSize => 14;
protected override float TextSize => 13;
protected override float LineHeight => 1;
protected override float Spacing => 5;
protected override float DateAlign => 125;
@ -198,9 +198,9 @@ namespace osu.Game.Online.Chat
protected partial class StandAloneMessage : ChatLine
{
protected override float FontSize => 15;
protected override float FontSize => 13;
protected override float Spacing => 5;
protected override float UsernameWidth => 75;
protected override float UsernameWidth => 90;
public StandAloneMessage(Message 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 VERSION_HASH_HEADER = @"X-Osu-Version-Hash";
public const string CLIENT_SESSION_ID_HEADER = @"X-Client-Session-ID";
/// <summary>
/// Invoked whenever a new hub connection is built, to configure it before it's started.
/// </summary>
@ -68,8 +71,11 @@ namespace osu.Game.Online
options.Proxy.Credentials = CredentialCache.DefaultCredentials;
}
options.Headers.Add("Authorization", $"Bearer {API.AccessToken}");
options.Headers.Add("OsuVersionHash", versionHash);
options.Headers.Add(@"Authorization", @$"Bearer {API.AccessToken}");
// 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)

View File

@ -25,5 +25,17 @@ namespace osu.Game.Online.Metadata
/// </summary>
[Key(1)]
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);
await connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence)).ConfigureAwait(false);
Schedule(() => isWatchingUserPresence.Value = true);
Logger.Log($@"{nameof(OnlineMetadataClient)} began watching user presence", LoggingTarget.Network);
}
public override async Task EndWatchingUserPresence()
@ -228,6 +229,7 @@ namespace osu.Game.Online.Metadata
Schedule(() => userStates.Clear());
Debug.Assert(connection != null);
await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false);
Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network);
}
finally
{
@ -247,7 +249,9 @@ namespace osu.Game.Online.Metadata
throw new OperationCanceledException();
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)
@ -257,6 +261,7 @@ namespace osu.Game.Online.Metadata
Debug.Assert(connection != null);
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()

View File

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

View File

@ -51,6 +51,7 @@ using osu.Game.Online.Chat;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Music;
using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.SkinEditor;
@ -62,6 +63,7 @@ using osu.Game.Screens;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
@ -83,7 +85,7 @@ namespace osu.Game
public partial class OsuGame : OsuGameBase, IKeyBindingHandler<GlobalAction>, ILocalUserPlayInfo, IPerformFromScreenRunner, IOverlayManager, ILinkHandler
{
#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;
#else
public const int IPC_PORT = 44823;
@ -132,6 +134,8 @@ namespace osu.Game
private Container topMostOverlayContent;
private Container footerBasedOverlayContent;
protected ScalingContainer ScreenContainer { 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 screenFooterOffset => (ScreenFooter?.DrawHeight ?? 0) - (ScreenFooter?.Position.Y ?? 0);
private IdleTracker idleTracker;
/// <summary>
@ -242,6 +244,10 @@ namespace osu.Game
throw new ArgumentException($@"{overlayContainer} has already been registered via {nameof(IOverlayManager.RegisterBlockingOverlay)} once.");
externalOverlays.Add(overlayContainer);
if (overlayContainer is ShearedOverlayContainer)
footerBasedOverlayContent.Add(overlayContainer);
else
overlayContent.Add(overlayContainer);
if (overlayContainer is OsuFocusedOverlayContainer focusedOverlayContainer)
@ -744,22 +750,35 @@ namespace osu.Game
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).
//
// 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
// 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 =
Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap) && Ruleset.Value.Equals(databasedScore.ScoreInfo.Ruleset)
? new[] { typeof(SongSelect) }
? new[] { typeof(SongSelect), typeof(DailyChallenge) }
: Array.Empty<Type>();
PerformFromScreen(screen =>
{
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;
if (!Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap))
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap);
switch (presentType)
@ -934,7 +953,6 @@ namespace osu.Game
return string.Join(" / ", combinations);
};
Container logoContainer;
ScreenFooter.BackReceptor backReceptor;
dependencies.CacheAs(idleTracker = new GameIdleTracker(6000));
@ -948,6 +966,8 @@ namespace osu.Game
Add(sessionIdleTracker);
Container logoContainer;
AddRange(new Drawable[]
{
new VolumeControlReceptor
@ -976,11 +996,19 @@ namespace osu.Game
Origin = Anchor.BottomLeft,
Action = () => ScreenFooter.OnBack?.Invoke(),
},
logoContainer = new Container { RelativeSizeAxes = Axes.Both },
footerBasedOverlayContent = new Container
{
Depth = -1,
RelativeSizeAxes = Axes.Both,
},
new PopoverContainer
{
Depth = -1,
RelativeSizeAxes = Axes.Both,
Child = ScreenFooter = new ScreenFooter(backReceptor)
{
RequestLogoInFront = inFront => ScreenContainer.ChangeChildDepth(logoContainer, inFront ? float.MinValue : 0),
OnBack = () =>
{
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)
{
dependencies.Cache(versionManager = new VersionManager { Depth = int.MinValue });
dependencies.Cache(versionManager = new VersionManager());
loadComponentSingleFile(versionManager, ScreenContainer.Add);
}
@ -1072,7 +1099,7 @@ namespace osu.Game
loadComponentSingleFile(CreateUpdateManager(), Add, true);
// overlay elements
loadComponentSingleFile(FirstRunOverlay = new FirstRunSetupOverlay(), overlayContent.Add, true);
loadComponentSingleFile(FirstRunOverlay = new FirstRunSetupOverlay(), footerBasedOverlayContent.Add, true);
loadComponentSingleFile(new ManageCollectionsDialog(), overlayContent.Add, true);
loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), 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.
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)
{
@ -1485,7 +1512,6 @@ namespace osu.Game
ScreenOffsetContainer.Padding = new MarginPadding { Top = toolbarOffset };
overlayOffsetContainer.Padding = new MarginPadding { Top = toolbarOffset };
ScreenStack.Padding = new MarginPadding { Bottom = screenFooterOffset };
float horizontalOffset = 0f;

View File

@ -20,8 +20,8 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osuTK;
using osuTK.Graphics;
using Message = osu.Game.Online.Chat.Message;
namespace osu.Game.Overlays.Chat
{
@ -47,11 +47,11 @@ namespace osu.Game.Overlays.Chat
public IReadOnlyCollection<Drawable> DrawableContentFlow => drawableContentFlow;
protected virtual float FontSize => 14;
protected virtual float FontSize => 12;
protected virtual float Spacing => 15;
protected virtual float UsernameWidth => 130;
protected virtual float UsernameWidth => 150;
[Resolved]
private ChannelManager? chatManager { get; set; }
@ -69,6 +69,41 @@ namespace osu.Game.Overlays.Chat
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>
/// The colour used to paint the author's username.
/// </summary>
@ -102,14 +137,36 @@ namespace osu.Game.Overlays.Chat
configManager.BindWith(OsuSetting.Prefer24HourTime, prefer24HourTime);
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,
AutoSizeAxes = Axes.Y,
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 45),
new Dimension(GridSizeMode.Absolute, Spacing + UsernameWidth + Spacing),
new Dimension(),
},
@ -120,9 +177,10 @@ namespace osu.Game.Overlays.Chat
drawableTimestamp = new OsuSpriteText
{
Shadow = false,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: FontSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true),
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Spacing = new Vector2(-1, 0),
Font = OsuFont.GetFont(size: FontSize, weight: FontWeight.SemiBold, fixedWidth: true),
AlwaysPresent = true,
},
drawableUsername = new DrawableChatUsername(message.Sender)
@ -143,7 +201,10 @@ namespace osu.Game.Overlays.Chat
}
},
}
}
};
updateBackground();
}
protected override void LoadComplete()
@ -203,9 +264,17 @@ namespace osu.Game.Overlays.Chat
private void updateMessageContent()
{
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();
}
else
{
drawableTimestamp.Hide();
}
drawableUsername.Text = $@"{message.Sender.Username}";
// remove non-existent channels from the link list
@ -217,7 +286,7 @@ namespace osu.Game.Overlays.Chat
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 =
@ -258,5 +327,11 @@ namespace osu.Game.Overlays.Chat
Color4Extensions.FromHex("812a96"),
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
{
protected virtual float TextSize => 15;
protected virtual float TextSize => 13;
protected virtual float LineHeight => 2;

View File

@ -61,7 +61,7 @@ namespace osu.Game.Overlays.Chat
Padding = new MarginPadding { Bottom = 5 },
Child = ChatLineFlow = new FillFlowContainer
{
Padding = new MarginPadding { Horizontal = 10 },
Padding = new MarginPadding { Left = 3, Right = 10 },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
@ -84,6 +84,25 @@ namespace osu.Game.Overlays.Chat
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>
/// Processes any pending message in <see cref="highlightedMessage"/>.
/// </summary>

View File

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

View File

@ -23,6 +23,7 @@ using osu.Game.Overlays.Settings;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osu.Game.Tests.Visual;
@ -153,6 +154,7 @@ namespace osu.Game.Overlays.FirstRunSetup
OsuScreenStack stack;
OsuLogo logo;
ScreenFooter footer;
Padding = new MarginPadding(5);
@ -166,7 +168,8 @@ namespace osu.Game.Overlays.FirstRunSetup
{
RelativePositionAxes = Axes.Both,
Position = new Vector2(0.5f),
})
}),
(typeof(ScreenFooter), footer = new ScreenFooter()),
},
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
@ -178,7 +181,8 @@ namespace osu.Game.Overlays.FirstRunSetup
Children = new Drawable[]
{
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.Notifications;
using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Menu;
namespace osu.Game.Overlays
@ -44,8 +45,7 @@ namespace osu.Game.Overlays
private ScreenStack? stack;
public ShearedButton NextButton = null!;
public ShearedButton BackButton = null!;
public ShearedButton? NextButton => DisplayedFooterContent?.NextButton;
private readonly Bindable<bool> showFirstRunSetup = new Bindable<bool>();
@ -90,7 +90,7 @@ namespace osu.Game.Overlays
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = 20, },
Padding = new MarginPadding { Bottom = 20 },
Child = new GridContainer
{
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()
@ -190,6 +145,36 @@ namespace osu.Game.Overlays
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)
{
if (!e.Repeat)
@ -197,19 +182,12 @@ namespace osu.Game.Overlays
switch (e.Action)
{
case GlobalAction.Select:
NextButton.TriggerClick();
DisplayedFooterContent?.NextButton.TriggerClick();
return true;
case GlobalAction.Back:
if (BackButton.Enabled.Value)
{
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;
footer.BackButton.TriggerClick();
return false;
}
}
@ -279,19 +257,6 @@ namespace osu.Game.Overlays
showNextStep();
}
private void showPreviousStep()
{
if (currentStepIndex == 0)
return;
Debug.Assert(stack != null);
stack.CurrentScreen.Exit();
currentStepIndex--;
updateButtons();
}
private void showNextStep()
{
Debug.Assert(currentStepIndex != null);
@ -322,29 +287,61 @@ namespace osu.Game.Overlays
updateButtons();
}
private void updateButtons()
{
BackButton.Enabled.Value = currentStepIndex > 0;
NextButton.Enabled.Value = currentStepIndex != null;
private void updateButtons() => DisplayedFooterContent?.UpdateButtons(currentStepIndex, steps);
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;
bool isFirstStep = currentStepIndex == 0;
bool isLastStep = currentStepIndex == steps.Count - 1;
bool isFirstStep = currentStep == 0;
bool isLastStep = currentStep == steps.Count - 1;
if (isFirstStep)
{
BackButton.Text = CommonStrings.Back;
NextButton.Text = FirstRunSetupOverlayStrings.GetStarted;
}
else
{
BackButton.Text = LocalisableString.Interpolate($@"{CommonStrings.Back} ({steps[currentStepIndex.Value - 1].GetLocalisableDescription()})");
NextButton.Text = isLastStep
? 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.
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics.CodeAnalysis;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@ -22,7 +23,7 @@ namespace osu.Game.Overlays
public virtual LocalisableString Title => Header.Title.Title;
public virtual LocalisableString Description => Header.Title.Description;
public T Header { get; }
public T Header { get; private set; }
protected virtual Color4 BackgroundColour => ColourProvider.Background5;
@ -34,11 +35,12 @@ namespace osu.Game.Overlays
protected override Container<Drawable> Content => content;
private readonly Box background;
private readonly Container content;
protected FullscreenOverlay(OverlayColourScheme colourScheme)
{
Header = CreateHeader();
RecreateHeader();
ColourProvider = new OverlayColourProvider(colourScheme);
@ -60,10 +62,9 @@ namespace osu.Game.Overlays
base.Content.AddRange(new Drawable[]
{
new Box
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = BackgroundColour
},
content = new Container
{
@ -75,14 +76,17 @@ namespace osu.Game.Overlays
[BackgroundDependencyLoader]
private void load()
{
Waves.FirstWaveColour = ColourProvider.Light4;
Waves.SecondWaveColour = ColourProvider.Light3;
Waves.ThirdWaveColour = ColourProvider.Dark4;
Waves.FourthWaveColour = ColourProvider.Dark3;
UpdateColours();
}
protected abstract T CreateHeader();
[MemberNotNull(nameof(Header))]
protected void RecreateHeader()
{
Header = CreateHeader();
}
public override void Show()
{
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()
{
base.PopIn();

View File

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

View File

@ -108,8 +108,6 @@ namespace osu.Game.Overlays.Mods
updateValues();
}, true);
BeatmapInfo.BindValueChanged(_ => updateValues());
Collapsed.BindValueChanged(_ =>
{
// 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.BindValueChanged(_ => updateValues());
BeatmapInfo.BindValueChanged(_ => updateValues());
BeatmapInfo.BindValueChanged(_ =>
{
updateStarDifficultyBindable();
updateValues();
}, true);
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)
{
startAnimating();
@ -154,17 +172,6 @@ namespace osu.Game.Overlays.Mods
if (BeatmapInfo.Value == null)
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);
bpmDisplay.Current.Value = FormatUtils.RoundBPM(BeatmapInfo.Value.BPM, rate);

View File

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

View File

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

View File

@ -138,6 +138,7 @@ namespace osu.Game.Overlays.Mods
},
new GridContainer
{
Padding = new MarginPadding { Top = 1, Bottom = 3 },
RelativeSizeAxes = Axes.Both,
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