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:
commit
7272747553
@ -21,7 +21,7 @@
|
||||
]
|
||||
},
|
||||
"ppy.localisationanalyser.tools": {
|
||||
"version": "2024.517.0",
|
||||
"version": "2024.802.0",
|
||||
"commands": [
|
||||
"localisation"
|
||||
]
|
||||
|
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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()
|
||||
|
@ -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++)
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -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>
|
||||
|
@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
AddNested(new SwellTick
|
||||
{
|
||||
StartTime = StartTime,
|
||||
Samples = Samples
|
||||
});
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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)));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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))]
|
||||
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
315
osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs
Normal file
315
osu.Game.Tests/Visual/Gameplay/TestScenePauseInputHandling.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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]
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)));
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>(),
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
|
86
osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs
Normal file
86
osu.Game.Tests/Visual/UserInterface/TestSceneOsuTooltip.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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()`
|
||||
|
@ -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()
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
29
osu.Game/Localisation/DailyChallengeStrings.cs
Normal file
29
osu.Game/Localisation/DailyChallengeStrings.cs
Normal 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's daily challenge has concluded – thanks for playing!
|
||||
///
|
||||
/// Tomorrow'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'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}";
|
||||
}
|
||||
}
|
@ -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(() =>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -157,6 +157,7 @@ namespace osu.Game.Overlays.Login
|
||||
},
|
||||
};
|
||||
|
||||
updateDropdownCurrent(status.Value);
|
||||
dropdown.Current.BindValueChanged(action =>
|
||||
{
|
||||
switch (action.NewValue)
|
||||
|
@ -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);
|
||||
|
@ -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 =>
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -138,6 +138,7 @@ namespace osu.Game.Overlays.Mods
|
||||
},
|
||||
new GridContainer
|
||||
{
|
||||
Padding = new MarginPadding { Top = 1, Bottom = 3 },
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RowDimensions = new[]
|
||||
{
|
||||
|
177
osu.Game/Overlays/Mods/ModSelectFooterContent.cs
Normal file
177
osu.Game/Overlays/Mods/ModSelectFooterContent.cs
Normal 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
Loading…
Reference in New Issue
Block a user