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

Merge branch 'ppy:master' into Freeze_frame_implementation

This commit is contained in:
MK56 2022-09-10 00:54:46 +02:00 committed by GitHub
commit 33a435e2ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
97 changed files with 2375 additions and 592 deletions

View File

@ -52,7 +52,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.831.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.831.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.901.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.908.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -0,0 +1,43 @@
// 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.Rulesets.Catch.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
public class TestSceneCatchTouchInput : OsuTestScene
{
private CatchTouchInputMapper catchTouchInputMapper = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create input overlay", () =>
{
Child = new CatchInputManager(new CatchRuleset().RulesetInfo)
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
catchTouchInputMapper = new CatchTouchInputMapper
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
}
}
};
});
}
[Test]
public void TestBasic()
{
AddStep("show overlay", () => catchTouchInputMapper.Show());
}
}
}

View File

@ -4,11 +4,13 @@
#nullable disable #nullable disable
using System.ComponentModel; using System.ComponentModel;
using osu.Framework.Allocation;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Catch namespace osu.Game.Rulesets.Catch
{ {
[Cached]
public class CatchInputManager : RulesetInputManager<CatchAction> public class CatchInputManager : RulesetInputManager<CatchAction>
{ {
public CatchInputManager(RulesetInfo ruleset) public CatchInputManager(RulesetInfo ruleset)

View File

@ -0,0 +1,277 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.UI
{
public class CatchTouchInputMapper : VisibilityContainer
{
public override bool PropagatePositionalInputSubTree => true;
public override bool PropagateNonPositionalInputSubTree => true;
private readonly Dictionary<object, TouchCatchAction> trackedActionSources = new Dictionary<object, TouchCatchAction>();
private KeyBindingContainer<CatchAction> keyBindingContainer = null!;
private Container mainContent = null!;
private InputArea leftBox = null!;
private InputArea rightBox = null!;
private InputArea leftDashBox = null!;
private InputArea rightDashBox = null!;
[BackgroundDependencyLoader]
private void load(CatchInputManager catchInputManager, OsuColour colours)
{
const float width = 0.15f;
keyBindingContainer = catchInputManager.KeyBindingContainer;
RelativeSizeAxes = Axes.Both;
Children = new Drawable[]
{
mainContent = new Container
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Width = width,
Children = new Drawable[]
{
leftDashBox = new InputArea(TouchCatchAction.DashLeft, trackedActionSources)
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
},
leftBox = new InputArea(TouchCatchAction.MoveLeft, trackedActionSources)
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Colour = colours.Gray9,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
},
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Width = width,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Children = new Drawable[]
{
rightBox = new InputArea(TouchCatchAction.MoveRight, trackedActionSources)
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Colour = colours.Gray9,
},
rightDashBox = new InputArea(TouchCatchAction.DashRight, trackedActionSources)
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
},
}
},
},
},
};
}
protected override bool OnKeyDown(KeyDownEvent e)
{
// Hide whenever the keyboard is used.
Hide();
return false;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
return updateAction(e.Button, getTouchCatchActionFromInput(e.ScreenSpaceMousePosition));
}
protected override bool OnTouchDown(TouchDownEvent e)
{
return updateAction(e.Touch.Source, getTouchCatchActionFromInput(e.ScreenSpaceTouch.Position));
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
Show();
TouchCatchAction? action = getTouchCatchActionFromInput(e.ScreenSpaceMousePosition);
// multiple mouse buttons may be pressed and handling the same action.
foreach (MouseButton button in e.PressedButtons)
updateAction(button, action);
return false;
}
protected override void OnTouchMove(TouchMoveEvent e)
{
updateAction(e.Touch.Source, getTouchCatchActionFromInput(e.ScreenSpaceTouch.Position));
base.OnTouchMove(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
updateAction(e.Button, null);
base.OnMouseUp(e);
}
protected override void OnTouchUp(TouchUpEvent e)
{
updateAction(e.Touch.Source, null);
base.OnTouchUp(e);
}
private bool updateAction(object source, TouchCatchAction? newAction)
{
TouchCatchAction? actionBefore = null;
if (trackedActionSources.TryGetValue(source, out TouchCatchAction found))
actionBefore = found;
if (actionBefore != newAction)
{
if (newAction != null)
trackedActionSources[source] = newAction.Value;
else
trackedActionSources.Remove(source);
updatePressedActions();
}
return newAction != null;
}
private void updatePressedActions()
{
Show();
if (trackedActionSources.ContainsValue(TouchCatchAction.DashLeft) || trackedActionSources.ContainsValue(TouchCatchAction.MoveLeft))
keyBindingContainer.TriggerPressed(CatchAction.MoveLeft);
else
keyBindingContainer.TriggerReleased(CatchAction.MoveLeft);
if (trackedActionSources.ContainsValue(TouchCatchAction.DashRight) || trackedActionSources.ContainsValue(TouchCatchAction.MoveRight))
keyBindingContainer.TriggerPressed(CatchAction.MoveRight);
else
keyBindingContainer.TriggerReleased(CatchAction.MoveRight);
if (trackedActionSources.ContainsValue(TouchCatchAction.DashLeft) || trackedActionSources.ContainsValue(TouchCatchAction.DashRight))
keyBindingContainer.TriggerPressed(CatchAction.Dash);
else
keyBindingContainer.TriggerReleased(CatchAction.Dash);
}
private TouchCatchAction? getTouchCatchActionFromInput(Vector2 screenSpaceInputPosition)
{
if (leftDashBox.Contains(screenSpaceInputPosition))
return TouchCatchAction.DashLeft;
if (rightDashBox.Contains(screenSpaceInputPosition))
return TouchCatchAction.DashRight;
if (leftBox.Contains(screenSpaceInputPosition))
return TouchCatchAction.MoveLeft;
if (rightBox.Contains(screenSpaceInputPosition))
return TouchCatchAction.MoveRight;
return null;
}
protected override void PopIn() => mainContent.FadeIn(300, Easing.OutQuint);
protected override void PopOut() => mainContent.FadeOut(300, Easing.OutQuint);
private class InputArea : CompositeDrawable, IKeyBindingHandler<CatchAction>
{
private readonly TouchCatchAction handledAction;
private readonly Box highlightOverlay;
private readonly IEnumerable<KeyValuePair<object, TouchCatchAction>> trackedActions;
private bool isHighlighted;
public InputArea(TouchCatchAction handledAction, IEnumerable<KeyValuePair<object, TouchCatchAction>> trackedActions)
{
this.handledAction = handledAction;
this.trackedActions = trackedActions;
InternalChildren = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 10,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.15f,
},
highlightOverlay = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Blending = BlendingParameters.Additive,
}
}
}
};
}
public bool OnPressed(KeyBindingPressEvent<CatchAction> _)
{
updateHighlight();
return false;
}
public void OnReleased(KeyBindingReleaseEvent<CatchAction> _)
{
updateHighlight();
}
private void updateHighlight()
{
bool isHandling = trackedActions.Any(a => a.Value == handledAction);
if (isHandling == isHighlighted)
return;
isHighlighted = isHandling;
highlightOverlay.FadeTo(isHighlighted ? 0.1f : 0, isHighlighted ? 80 : 400, Easing.OutQuint);
}
}
public enum TouchCatchAction
{
MoveLeft,
MoveRight,
DashLeft,
DashRight,
}
}
}

View File

@ -4,6 +4,7 @@
#nullable disable #nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -32,6 +33,12 @@ namespace osu.Game.Rulesets.Catch.UI
TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450); TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450);
} }
[BackgroundDependencyLoader]
private void load()
{
KeyBindingInputManager.Add(new CatchTouchInputMapper());
}
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield); protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield);

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using osu.Framework.Configuration.Tracking; using osu.Framework.Configuration.Tracking;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -33,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
scrollTime => new SettingDescription( scrollTime => new SettingDescription(
rawValue: scrollTime, rawValue: scrollTime,
name: "Scroll Speed", name: "Scroll Speed",
value: $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)} ({scrollTime}ms)" value: $"{scrollTime}ms (speed {(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)})"
) )
) )
}; };

View File

@ -1,8 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
@ -34,7 +33,7 @@ namespace osu.Game.Rulesets.Mania
LabelText = "Scrolling direction", LabelText = "Scrolling direction",
Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection) Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection)
}, },
new SettingsSlider<double, TimeSlider> new SettingsSlider<double, ManiaScrollSlider>
{ {
LabelText = "Scroll speed", LabelText = "Scroll speed",
Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime), Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime),
@ -47,5 +46,10 @@ namespace osu.Game.Rulesets.Mania
} }
}; };
} }
private class ManiaScrollSlider : OsuSliderBar<double>
{
public override LocalisableString TooltipText => $"{Current.Value}ms (speed {(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / Current.Value)})";
}
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Utils; using osu.Framework.Utils;
@ -9,6 +10,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Osu.Utils; using osu.Game.Rulesets.Osu.Utils;
@ -25,40 +27,100 @@ namespace osu.Game.Rulesets.Osu.Mods
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast; private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
private Random? rng; private Random random = null!;
public void ApplyToBeatmap(IBeatmap beatmap) public void ApplyToBeatmap(IBeatmap beatmap)
{ {
if (!(beatmap is OsuBeatmap osuBeatmap)) if (beatmap is not OsuBeatmap osuBeatmap)
return; return;
Seed.Value ??= RNG.Next(); Seed.Value ??= RNG.Next();
rng = new Random((int)Seed.Value); random = new Random((int)Seed.Value);
var positionInfos = OsuHitObjectGenerationUtils.GeneratePositionInfos(osuBeatmap.HitObjects); var positionInfos = OsuHitObjectGenerationUtils.GeneratePositionInfos(osuBeatmap.HitObjects);
float rateOfChangeMultiplier = 0; // Offsets the angles of all hit objects in a "section" by the same amount.
float sectionOffset = 0;
foreach (var positionInfo in positionInfos) // Whether the angles are positive or negative (clockwise or counter-clockwise flow).
bool flowDirection = false;
for (int i = 0; i < positionInfos.Count; i++)
{ {
// rateOfChangeMultiplier only changes every 5 iterations in a combo if (shouldStartNewSection(osuBeatmap, positionInfos, i))
// to prevent shaky-line-shaped streams
if (positionInfo.HitObject.IndexInCurrentCombo % 5 == 0)
rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1;
if (positionInfo == positionInfos.First())
{ {
positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2); sectionOffset = OsuHitObjectGenerationUtils.RandomGaussian(random, 0, 0.0008f);
positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); flowDirection = !flowDirection;
}
if (i == 0)
{
positionInfos[i].DistanceFromPrevious = (float)(random.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2);
positionInfos[i].RelativeAngle = (float)(random.NextDouble() * 2 * Math.PI - Math.PI);
} }
else else
{ {
positionInfo.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, positionInfo.DistanceFromPrevious / (playfield_diagonal * 0.5f)); // Offsets only the angle of the current hit object if a flow change occurs.
float flowChangeOffset = 0;
// Offsets only the angle of the current hit object.
float oneTimeOffset = OsuHitObjectGenerationUtils.RandomGaussian(random, 0, 0.002f);
if (shouldApplyFlowChange(positionInfos, i))
{
flowChangeOffset = OsuHitObjectGenerationUtils.RandomGaussian(random, 0, 0.002f);
flowDirection = !flowDirection;
}
float totalOffset =
// sectionOffset and oneTimeOffset should mainly affect patterns with large spacing.
(sectionOffset + oneTimeOffset) * positionInfos[i].DistanceFromPrevious +
// flowChangeOffset should mainly affect streams.
flowChangeOffset * (playfield_diagonal - positionInfos[i].DistanceFromPrevious);
positionInfos[i].RelativeAngle = getRelativeTargetAngle(positionInfos[i].DistanceFromPrevious, totalOffset, flowDirection);
} }
} }
osuBeatmap.HitObjects = OsuHitObjectGenerationUtils.RepositionHitObjects(positionInfos); osuBeatmap.HitObjects = OsuHitObjectGenerationUtils.RepositionHitObjects(positionInfos);
} }
/// <param name="targetDistance">The target distance between the previous and the current <see cref="OsuHitObject"/>.</param>
/// <param name="offset">The angle (in rad) by which the target angle should be offset.</param>
/// <param name="flowDirection">Whether the relative angle should be positive or negative.</param>
private static float getRelativeTargetAngle(float targetDistance, float offset, bool flowDirection)
{
float angle = (float)(2.16 / (1 + 200 * Math.Exp(0.036 * (targetDistance - 310))) + 0.5 + offset);
float relativeAngle = (float)Math.PI - angle;
return flowDirection ? -relativeAngle : relativeAngle;
}
/// <returns>Whether a new section should be started at the current <see cref="OsuHitObject"/>.</returns>
private bool shouldStartNewSection(OsuBeatmap beatmap, IReadOnlyList<OsuHitObjectGenerationUtils.ObjectPositionInfo> positionInfos, int i)
{
if (i == 0)
return true;
// Exclude new-combo-spam and 1-2-combos.
bool previousObjectStartedCombo = positionInfos[Math.Max(0, i - 2)].HitObject.IndexInCurrentCombo > 1 &&
positionInfos[i - 1].HitObject.NewCombo;
bool previousObjectWasOnDownbeat = OsuHitObjectGenerationUtils.IsHitObjectOnBeat(beatmap, positionInfos[i - 1].HitObject, true);
bool previousObjectWasOnBeat = OsuHitObjectGenerationUtils.IsHitObjectOnBeat(beatmap, positionInfos[i - 1].HitObject);
return (previousObjectStartedCombo && random.NextDouble() < 0.6f) ||
previousObjectWasOnDownbeat ||
(previousObjectWasOnBeat && random.NextDouble() < 0.4f);
}
/// <returns>Whether a flow change should be applied at the current <see cref="OsuHitObject"/>.</returns>
private bool shouldApplyFlowChange(IReadOnlyList<OsuHitObjectGenerationUtils.ObjectPositionInfo> positionInfos, int i)
{
// Exclude new-combo-spam and 1-2-combos.
bool previousObjectStartedCombo = positionInfos[Math.Max(0, i - 2)].HitObject.IndexInCurrentCombo > 1 &&
positionInfos[i - 1].HitObject.NewCombo;
return previousObjectStartedCombo && random.NextDouble() < 0.6f;
}
} }
} }

View File

@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
currentRotation += angle; currentRotation += angle;
// rate has to be applied each frame, because it's not guaranteed to be constant throughout playback // rate has to be applied each frame, because it's not guaranteed to be constant throughout playback
// (see: ModTimeRamp) // (see: ModTimeRamp)
drawableSpinner.Result.RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.TrueGameplayRate ?? Clock.Rate)); drawableSpinner.Result.RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate));
} }
private void resetState(DrawableHitObject obj) private void resetState(DrawableHitObject obj)

View File

@ -8,6 +8,7 @@ using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osuTK; using osuTK;
@ -186,5 +187,39 @@ namespace osu.Game.Rulesets.Osu.Utils
length * MathF.Sin(angle) length * MathF.Sin(angle)
); );
} }
/// <param name="beatmap">The beatmap hitObject is a part of.</param>
/// <param name="hitObject">The <see cref="OsuHitObject"/> that should be checked.</param>
/// <param name="downbeatsOnly">If true, this method only returns true if hitObject is on a downbeat.
/// If false, it returns true if hitObject is on any beat.</param>
/// <returns>true if hitObject is on a (down-)beat, false otherwise.</returns>
public static bool IsHitObjectOnBeat(OsuBeatmap beatmap, OsuHitObject hitObject, bool downbeatsOnly = false)
{
var timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
double timeSinceTimingPoint = hitObject.StartTime - timingPoint.Time;
double beatLength = timingPoint.BeatLength;
if (downbeatsOnly)
beatLength *= timingPoint.TimeSignature.Numerator;
// Ensure within 1ms of expected location.
return Math.Abs(timeSinceTimingPoint + 1) % beatLength < 2;
}
/// <summary>
/// Generates a random number from a normal distribution using the Box-Muller transform.
/// </summary>
public static float RandomGaussian(Random rng, float mean = 0, float stdDev = 1)
{
// Generate 2 random numbers in the interval (0,1].
// x1 must not be 0 since log(0) = undefined.
double x1 = 1 - rng.NextDouble();
double x2 = 1 - rng.NextDouble();
double stdNormal = Math.Sqrt(-2 * Math.Log(x1)) * Math.Sin(2 * Math.PI * x2);
return mean + stdDev * (float)stdNormal;
}
} }
} }

View File

@ -1,8 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -13,21 +14,20 @@ namespace osu.Game.Tests.NonVisual
{ {
[TestCase(0)] [TestCase(0)]
[TestCase(1)] [TestCase(1)]
public void TestTrueGameplayRateWithZeroAdjustment(double underlyingClockRate) public void TestTrueGameplayRateWithGameplayAdjustment(double underlyingClockRate)
{ {
var framedClock = new FramedClock(new ManualClock { Rate = underlyingClockRate }); var framedClock = new FramedClock(new ManualClock { Rate = underlyingClockRate });
var gameplayClock = new TestGameplayClockContainer(framedClock); var gameplayClock = new TestGameplayClockContainer(framedClock);
Assert.That(gameplayClock.TrueGameplayRate, Is.EqualTo(0)); Assert.That(gameplayClock.GetTrueGameplayRate(), Is.EqualTo(2));
} }
private class TestGameplayClockContainer : GameplayClockContainer private class TestGameplayClockContainer : GameplayClockContainer
{ {
public override IEnumerable<double> NonGameplayAdjustments => new[] { 0.0 };
public TestGameplayClockContainer(IFrameBasedClock underlyingClock) public TestGameplayClockContainer(IFrameBasedClock underlyingClock)
: base(underlyingClock) : base(underlyingClock)
{ {
AdjustmentsFromMods.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(2.0));
} }
} }
} }

View File

@ -36,7 +36,9 @@ namespace osu.Game.Tests.Skins
"Archives/modified-default-20220723.osk", "Archives/modified-default-20220723.osk",
"Archives/modified-classic-20220723.osk", "Archives/modified-classic-20220723.osk",
// Covers legacy song progress, UR counter, colour hit error metre. // Covers legacy song progress, UR counter, colour hit error metre.
"Archives/modified-classic-20220801.osk" "Archives/modified-classic-20220801.osk",
// Covers clicks/s counter
"Archives/modified-default-20220818.osk"
}; };
/// <summary> /// <summary>

View File

@ -15,12 +15,14 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Beatmaps.Drawables.Cards.Buttons;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Beatmaps namespace osu.Game.Tests.Visual.Beatmaps
{ {
@ -295,5 +297,22 @@ namespace osu.Game.Tests.Visual.Beatmaps
BeatmapCardNormal firstCard() => this.ChildrenOfType<BeatmapCardNormal>().First(); BeatmapCardNormal firstCard() => this.ChildrenOfType<BeatmapCardNormal>().First();
} }
[Test]
public void TestPlayButtonByTouchInput()
{
AddStep("create cards", () => Child = createContent(OverlayColourScheme.Blue, beatmapSetInfo => new BeatmapCardNormal(beatmapSetInfo)));
// mimics touch input
AddStep("touch play button area on first card", () =>
{
InputManager.MoveMouseTo(firstCard().ChildrenOfType<PlayButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("first card is playing", () => firstCard().ChildrenOfType<PlayButton>().Single().Playing.Value);
BeatmapCardNormal firstCard() => this.ChildrenOfType<BeatmapCardNormal>().First();
}
} }
} }

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables.Cards.Buttons; using osu.Game.Beatmaps.Drawables.Cards.Buttons;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
@ -58,6 +59,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
State = { Value = DownloadState.NotDownloaded },
Scale = new Vector2(2) Scale = new Vector2(2)
}; };
}); });

View File

@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
}; };
}); });
AddStep("enable dim", () => thumbnail.Dimmed.Value = true); AddStep("enable dim", () => thumbnail.Dimmed.Value = true);
AddUntilStep("button visible", () => playButton.IsPresent); AddUntilStep("button visible", () => playButton.Alpha == 1);
AddStep("click button", () => AddStep("click button", () =>
{ {
@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
AddStep("disable dim", () => thumbnail.Dimmed.Value = false); AddStep("disable dim", () => thumbnail.Dimmed.Value = false);
AddWaitStep("wait some", 3); AddWaitStep("wait some", 3);
AddAssert("button still visible", () => playButton.IsPresent); AddAssert("button still visible", () => playButton.Alpha == 1);
// The track plays in real-time, so we need to check for progress in increments to avoid timeout. // The track plays in real-time, so we need to check for progress in increments to avoid timeout.
AddUntilStep("progress > 0.25", () => thumbnail.ChildrenOfType<PlayButton>().Single().Progress.Value > 0.25); AddUntilStep("progress > 0.25", () => thumbnail.ChildrenOfType<PlayButton>().Single().Progress.Value > 0.25);
@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
AddUntilStep("progress > 0.75", () => thumbnail.ChildrenOfType<PlayButton>().Single().Progress.Value > 0.75); AddUntilStep("progress > 0.75", () => thumbnail.ChildrenOfType<PlayButton>().Single().Progress.Value > 0.75);
AddUntilStep("wait for track to end", () => !playButton.Playing.Value); AddUntilStep("wait for track to end", () => !playButton.Playing.Value);
AddUntilStep("button hidden", () => !playButton.IsPresent); AddUntilStep("button hidden", () => playButton.Alpha == 0);
} }
private void iconIs(IconUsage usage) => AddUntilStep("icon is correct", () => playButton.ChildrenOfType<SpriteIcon>().Any(icon => icon.Icon.Equals(usage))); private void iconIs(IconUsage usage) => AddUntilStep("icon is correct", () => playButton.ChildrenOfType<SpriteIcon>().Any(icon => icon.Icon.Equals(usage)));

View File

@ -0,0 +1,130 @@
// 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.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneClicksPerSecondCalculator : OsuTestScene
{
private ClicksPerSecondCalculator calculator = null!;
private TestGameplayClock manualGameplayClock = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create components", () =>
{
manualGameplayClock = new TestGameplayClock();
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[] { (typeof(IGameplayClock), manualGameplayClock) },
Children = new Drawable[]
{
calculator = new ClicksPerSecondCalculator(),
new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[] { (typeof(ClicksPerSecondCalculator), calculator) },
Child = new ClicksPerSecondCounter
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(5),
}
}
},
};
});
}
[Test]
public void TestBasicConsistency()
{
seek(1000);
AddStep("add inputs in past", () => addInputs(new double[] { 0, 100, 200, 300, 400, 500, 600, 700, 800, 900 }));
checkClicksPerSecondValue(10);
}
[Test]
public void TestRateAdjustConsistency()
{
seek(1000);
AddStep("add inputs in past", () => addInputs(new double[] { 0, 100, 200, 300, 400, 500, 600, 700, 800, 900 }));
checkClicksPerSecondValue(10);
AddStep("set rate 0.5x", () => manualGameplayClock.TrueGameplayRate = 0.5);
checkClicksPerSecondValue(5);
}
[Test]
public void TestInputsDiscardedOnRewind()
{
seek(1000);
AddStep("add inputs in past", () => addInputs(new double[] { 0, 100, 200, 300, 400, 500, 600, 700, 800, 900 }));
checkClicksPerSecondValue(10);
seek(500);
checkClicksPerSecondValue(6);
seek(1000);
checkClicksPerSecondValue(6);
}
private void checkClicksPerSecondValue(int i) => AddAssert("clicks/s is correct", () => calculator.Value, () => Is.EqualTo(i));
private void seekClockImmediately(double time) => manualGameplayClock.CurrentTime = time;
private void seek(double time) => AddStep($"Seek to {time}ms", () => seekClockImmediately(time));
private void addInputs(IEnumerable<double> inputs)
{
double baseTime = manualGameplayClock.CurrentTime;
foreach (double timestamp in inputs)
{
seekClockImmediately(timestamp);
calculator.AddInputTimestamp();
}
seekClockImmediately(baseTime);
}
private class TestGameplayClock : IGameplayClock
{
public double CurrentTime { get; set; }
public double Rate => 1;
public bool IsRunning => true;
public double TrueGameplayRate { set => adjustableAudioComponent.Tempo.Value = value; }
private readonly AudioAdjustments adjustableAudioComponent = new AudioAdjustments();
public void ProcessFrame()
{
}
public double ElapsedFrameTime => throw new NotImplementedException();
public double FramesPerSecond => throw new NotImplementedException();
public FrameTimeInfo TimeInfo => throw new NotImplementedException();
public double StartTime => throw new NotImplementedException();
public IAdjustableAudioComponent AdjustmentsFromMods => adjustableAudioComponent;
public IEnumerable<double> NonGameplayAdjustments => throw new NotImplementedException();
public IBindable<bool> IsPaused => throw new NotImplementedException();
}
}
}

View File

@ -78,8 +78,9 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true); AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true);
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
AddAssert("sprites present", () => sprites.All(s => s.IsPresent));
AddStep("scale sprite", () => sprites.ForEach(s => s.VectorScale = new Vector2(0, 1))); AddStep("scale sprite", () => sprites.ForEach(s => s.VectorScale = new Vector2(0, 1)));
AddAssert("zero width", () => sprites.All(s => s.ScreenSpaceDrawQuad.Width == 0)); AddAssert("sprites not present", () => sprites.All(s => !s.IsPresent));
} }
[Test] [Test]

View File

@ -370,7 +370,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void confirmNoTrackAdjustments() private void confirmNoTrackAdjustments()
{ {
AddAssert("track has no adjustments", () => Beatmap.Value.Track.AggregateFrequency.Value == 1); AddUntilStep("track has no adjustments", () => Beatmap.Value.Track.AggregateFrequency.Value, () => Is.EqualTo(1));
} }
private void restart() => AddStep("restart", () => Player.Restart()); private void restart() => AddStep("restart", () => Player.Restart());

View File

@ -81,9 +81,11 @@ namespace osu.Game.Tests.Visual.Gameplay
CreateTest(); CreateTest();
AddUntilStep("fail screen displayed", () => Player.ChildrenOfType<FailOverlay>().First().State.Value == Visibility.Visible); AddUntilStep("fail screen displayed", () => Player.ChildrenOfType<FailOverlay>().First().State.Value == Visibility.Visible);
AddUntilStep("wait for button clickable", () => Player.ChildrenOfType<SaveFailedScoreButton>().First().ChildrenOfType<OsuClickableContainer>().First().Enabled.Value);
AddUntilStep("score not in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) == null)); AddUntilStep("score not in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) == null));
AddStep("click save button", () => Player.ChildrenOfType<SaveFailedScoreButton>().First().ChildrenOfType<OsuClickableContainer>().First().TriggerClick()); AddStep("click save button", () => Player.ChildrenOfType<SaveFailedScoreButton>().First().ChildrenOfType<OsuClickableContainer>().First().TriggerClick());
AddUntilStep("score not in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null)); AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
} }
[Test] [Test]

View File

@ -202,7 +202,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for load", () => downloadButton.IsLoaded); AddUntilStep("wait for load", () => downloadButton.IsLoaded);
AddAssert("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded); AddAssert("state is unknown", () => downloadButton.State.Value == DownloadState.Unknown);
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value); AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -22,7 +20,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestSceneSkinEditor : PlayerTestScene public class TestSceneSkinEditor : PlayerTestScene
{ {
private SkinEditor skinEditor; private SkinEditor? skinEditor;
protected override bool Autoplay => true; protected override bool Autoplay => true;
@ -42,29 +40,33 @@ namespace osu.Game.Tests.Visual.Gameplay
Player.ScaleTo(0.4f); Player.ScaleTo(0.4f);
LoadComponentAsync(skinEditor = new SkinEditor(Player), Add); LoadComponentAsync(skinEditor = new SkinEditor(Player), Add);
}); });
AddUntilStep("wait for loaded", () => skinEditor.IsLoaded); AddUntilStep("wait for loaded", () => skinEditor!.IsLoaded);
} }
[Test] [Test]
public void TestToggleEditor() public void TestToggleEditor()
{ {
AddToggleStep("toggle editor visibility", _ => skinEditor.ToggleVisibility()); AddToggleStep("toggle editor visibility", _ => skinEditor!.ToggleVisibility());
} }
[Test] [Test]
public void TestEditComponent() public void TestEditComponent()
{ {
BarHitErrorMeter hitErrorMeter = null; BarHitErrorMeter hitErrorMeter = null!;
AddStep("select bar hit error blueprint", () => AddStep("select bar hit error blueprint", () =>
{ {
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().First(b => b.Item is BarHitErrorMeter); var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().First(b => b.Item is BarHitErrorMeter);
hitErrorMeter = (BarHitErrorMeter)blueprint.Item; hitErrorMeter = (BarHitErrorMeter)blueprint.Item;
skinEditor.SelectedComponents.Clear(); skinEditor!.SelectedComponents.Clear();
skinEditor.SelectedComponents.Add(blueprint.Item); skinEditor.SelectedComponents.Add(blueprint.Item);
}); });
AddStep("move by keyboard", () => InputManager.Key(Key.Right));
AddAssert("hitErrorMeter moved", () => hitErrorMeter.X != 0);
AddAssert("value is default", () => hitErrorMeter.JudgementLineThickness.IsDefault); AddAssert("value is default", () => hitErrorMeter.JudgementLineThickness.IsDefault);
AddStep("hover first slider", () => AddStep("hover first slider", () =>

View File

@ -91,8 +91,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
break; break;
case StopCountdownRequest: case StopCountdownRequest:
multiplayerRoom.Countdown = null; clearRoomCountdown();
raiseRoomUpdated();
break; break;
} }
}); });
@ -244,14 +243,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
}); });
AddStep("start countdown", () => multiplayerClient.Object.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely()); AddStep("start countdown", () => multiplayerClient.Object.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely());
AddUntilStep("countdown started", () => multiplayerRoom.Countdown != null); AddUntilStep("countdown started", () => multiplayerRoom.ActiveCountdowns.Any());
AddStep("transfer host to local user", () => transferHost(localUser)); AddStep("transfer host to local user", () => transferHost(localUser));
AddUntilStep("local user is host", () => multiplayerRoom.Host?.Equals(multiplayerClient.Object.LocalUser) == true); AddUntilStep("local user is host", () => multiplayerRoom.Host?.Equals(multiplayerClient.Object.LocalUser) == true);
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
checkLocalUserState(MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
AddAssert("countdown still active", () => multiplayerRoom.Countdown != null); AddAssert("countdown still active", () => multiplayerRoom.ActiveCountdowns.Any());
} }
[Test] [Test]
@ -392,7 +391,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void setRoomCountdown(TimeSpan duration) private void setRoomCountdown(TimeSpan duration)
{ {
multiplayerRoom.Countdown = new MatchStartCountdown { TimeRemaining = duration }; multiplayerRoom.ActiveCountdowns.Add(new MatchStartCountdown { TimeRemaining = duration });
raiseRoomUpdated();
}
private void clearRoomCountdown()
{
multiplayerRoom.ActiveCountdowns.Clear();
raiseRoomUpdated(); raiseRoomUpdated();
} }

View File

@ -13,9 +13,11 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -332,6 +334,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("player 2 playing from correct point in time", () => getPlayer(PLAYER_2_ID).ChildrenOfType<DrawableRuleset>().Single().FrameStableClock.CurrentTime > 30000); AddUntilStep("player 2 playing from correct point in time", () => getPlayer(PLAYER_2_ID).ChildrenOfType<DrawableRuleset>().Single().FrameStableClock.CurrentTime > 30000);
} }
[Test]
public void TestGameplayRateAdjust()
{
start(getPlayerIds(4), mods: new[] { new APIMod(new OsuModDoubleTime()) });
loadSpectateScreen();
sendFrames(getPlayerIds(4), 300);
AddUntilStep("wait for correct track speed", () => Beatmap.Value.Track.Rate, () => Is.EqualTo(1.5));
}
[Test] [Test]
public void TestPlayersLeaveWhileSpectating() public void TestPlayersLeaveWhileSpectating()
{ {
@ -420,7 +434,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId); private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId);
private void start(int[] userIds, int? beatmapId = null) private void start(int[] userIds, int? beatmapId = null, APIMod[]? mods = null)
{ {
AddStep("start play", () => AddStep("start play", () =>
{ {
@ -429,10 +443,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
var user = new MultiplayerRoomUser(id) var user = new MultiplayerRoomUser(id)
{ {
User = new APIUser { Id = id }, User = new APIUser { Id = id },
Mods = mods ?? Array.Empty<APIMod>(),
}; };
OnlinePlayDependencies.MultiplayerClient.AddUser(user.User, true); OnlinePlayDependencies.MultiplayerClient.AddUser(user, true);
SpectatorClient.SendStartPlay(id, beatmapId ?? importedBeatmapId); SpectatorClient.SendStartPlay(id, beatmapId ?? importedBeatmapId, mods);
playingUsers.Add(user); playingUsers.Add(user);
} }

View File

@ -19,7 +19,6 @@ using osu.Game.Database;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
@ -68,37 +67,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded); AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded);
} }
[Test]
public void TestBeatmapRevertedOnExitIfNoSelection()
{
BeatmapInfo selectedBeatmap = null;
AddStep("select beatmap",
() => songSelect.Carousel.SelectBeatmap(selectedBeatmap = beatmaps.Where(beatmap => beatmap.Ruleset.OnlineID == new OsuRuleset().LegacyID).ElementAt(1)));
AddUntilStep("wait for selection", () => Beatmap.Value.BeatmapInfo.Equals(selectedBeatmap));
AddStep("exit song select", () => songSelect.Exit());
AddAssert("beatmap reverted", () => Beatmap.IsDefault);
}
[Test]
public void TestModsRevertedOnExitIfNoSelection()
{
AddStep("change mods", () => SelectedMods.Value = new[] { new OsuModDoubleTime() });
AddStep("exit song select", () => songSelect.Exit());
AddAssert("mods reverted", () => SelectedMods.Value.Count == 0);
}
[Test]
public void TestRulesetRevertedOnExitIfNoSelection()
{
AddStep("change ruleset", () => Ruleset.Value = new CatchRuleset().RulesetInfo);
AddStep("exit song select", () => songSelect.Exit());
AddAssert("ruleset reverted", () => Ruleset.Value.Equals(new OsuRuleset().RulesetInfo));
}
[Test] [Test]
public void TestBeatmapConfirmed() public void TestBeatmapConfirmed()
{ {
@ -152,8 +120,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
public new BeatmapCarousel Carousel => base.Carousel; public new BeatmapCarousel Carousel => base.Carousel;
public TestMultiplayerMatchSongSelect(Room room, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null) public TestMultiplayerMatchSongSelect(Room room)
: base(room, null, beatmap, ruleset) : base(room)
{ {
} }
} }

View File

@ -154,7 +154,7 @@ namespace osu.Game.Tests.Visual.Online
}); });
} }
private int onlineID = 1; private ulong onlineID = 1;
private APIScoresCollection createScores() private APIScoresCollection createScores()
{ {

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -20,7 +18,7 @@ namespace osu.Game.Tests.Visual.Ranking
{ {
public class TestSceneHitEventTimingDistributionGraph : OsuTestScene public class TestSceneHitEventTimingDistributionGraph : OsuTestScene
{ {
private HitEventTimingDistributionGraph graph; private HitEventTimingDistributionGraph graph = null!;
private static readonly HitObject placeholder_object = new HitCircle(); private static readonly HitObject placeholder_object = new HitCircle();
@ -43,6 +41,65 @@ namespace osu.Game.Tests.Visual.Ranking
createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList()); createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList());
} }
[Test]
public void TestSparse()
{
createTest(new List<HitEvent>
{
new HitEvent(-7, HitResult.Perfect, placeholder_object, placeholder_object, null),
new HitEvent(-6, HitResult.Perfect, placeholder_object, placeholder_object, null),
new HitEvent(-5, HitResult.Perfect, placeholder_object, placeholder_object, null),
new HitEvent(5, HitResult.Perfect, placeholder_object, placeholder_object, null),
new HitEvent(6, HitResult.Perfect, placeholder_object, placeholder_object, null),
new HitEvent(7, HitResult.Perfect, placeholder_object, placeholder_object, null),
});
}
[Test]
public void TestVariousTypesOfHitResult()
{
createTest(CreateDistributedHitEvents(0, 50).Select(h =>
{
double offset = Math.Abs(h.TimeOffset);
HitResult result = offset > 36 ? HitResult.Miss
: offset > 32 ? HitResult.Meh
: offset > 24 ? HitResult.Ok
: offset > 16 ? HitResult.Good
: offset > 8 ? HitResult.Great
: HitResult.Perfect;
return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null);
}).ToList());
}
[Test]
public void TestMultipleWindowsOfHitResult()
{
var wide = CreateDistributedHitEvents(0, 50).Select(h =>
{
double offset = Math.Abs(h.TimeOffset);
HitResult result = offset > 36 ? HitResult.Miss
: offset > 32 ? HitResult.Meh
: offset > 24 ? HitResult.Ok
: offset > 16 ? HitResult.Good
: offset > 8 ? HitResult.Great
: HitResult.Perfect;
return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null);
});
var narrow = CreateDistributedHitEvents(0, 50).Select(h =>
{
double offset = Math.Abs(h.TimeOffset);
HitResult result = offset > 25 ? HitResult.Miss
: offset > 20 ? HitResult.Meh
: offset > 15 ? HitResult.Ok
: offset > 10 ? HitResult.Good
: offset > 5 ? HitResult.Great
: HitResult.Perfect;
return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null);
});
createTest(wide.Concat(narrow).ToList());
}
[Test] [Test]
public void TestZeroTimeOffset() public void TestZeroTimeOffset()
{ {

View File

@ -1,10 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
@ -13,6 +12,7 @@ using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
@ -46,11 +46,12 @@ namespace osu.Game.Tests.Visual.SongSelect
[TestFixture] [TestFixture]
public class TestScenePlaySongSelect : ScreenTestScene public class TestScenePlaySongSelect : ScreenTestScene
{ {
private BeatmapManager manager; private BeatmapManager manager = null!;
private RulesetStore rulesets; private RulesetStore rulesets = null!;
private MusicController music; private MusicController music = null!;
private WorkingBeatmap defaultBeatmap; private WorkingBeatmap defaultBeatmap = null!;
private TestSongSelect songSelect; private OsuConfigManager config = null!;
private TestSongSelect? songSelect;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio) private void load(GameHost host, AudioManager audio)
@ -69,8 +70,6 @@ namespace osu.Game.Tests.Visual.SongSelect
Dependencies.Cache(config = new OsuConfigManager(LocalStorage)); Dependencies.Cache(config = new OsuConfigManager(LocalStorage));
} }
private OsuConfigManager config;
public override void SetUpSteps() public override void SetUpSteps()
{ {
base.SetUpSteps(); base.SetUpSteps();
@ -85,7 +84,7 @@ namespace osu.Game.Tests.Visual.SongSelect
songSelect = null; songSelect = null;
}); });
AddStep("delete all beatmaps", () => manager?.Delete()); AddStep("delete all beatmaps", () => manager.Delete());
} }
[Test] [Test]
@ -98,7 +97,7 @@ namespace osu.Game.Tests.Visual.SongSelect
addRulesetImportStep(0); addRulesetImportStep(0);
AddUntilStep("wait for placeholder hidden", () => getPlaceholder()?.State.Value == Visibility.Hidden); AddUntilStep("wait for placeholder hidden", () => getPlaceholder()?.State.Value == Visibility.Hidden);
AddStep("delete all beatmaps", () => manager?.Delete()); AddStep("delete all beatmaps", () => manager.Delete());
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible); AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
} }
@ -144,7 +143,7 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect(); createSongSelect();
AddAssert("filter count is 1", () => songSelect.FilterCount == 1); AddAssert("filter count is 1", () => songSelect?.FilterCount == 1);
} }
[Test] [Test]
@ -156,7 +155,7 @@ namespace osu.Game.Tests.Visual.SongSelect
waitForInitialSelection(); waitForInitialSelection();
WorkingBeatmap selected = null; WorkingBeatmap? selected = null;
AddStep("store selected beatmap", () => selected = Beatmap.Value); AddStep("store selected beatmap", () => selected = Beatmap.Value);
@ -166,7 +165,7 @@ namespace osu.Game.Tests.Visual.SongSelect
InputManager.Key(Key.Enter); InputManager.Key(Key.Enter);
}); });
AddUntilStep("wait for not current", () => !songSelect.IsCurrentScreen()); AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
AddAssert("ensure selection changed", () => selected != Beatmap.Value); AddAssert("ensure selection changed", () => selected != Beatmap.Value);
} }
@ -179,7 +178,7 @@ namespace osu.Game.Tests.Visual.SongSelect
waitForInitialSelection(); waitForInitialSelection();
WorkingBeatmap selected = null; WorkingBeatmap? selected = null;
AddStep("store selected beatmap", () => selected = Beatmap.Value); AddStep("store selected beatmap", () => selected = Beatmap.Value);
@ -189,7 +188,7 @@ namespace osu.Game.Tests.Visual.SongSelect
InputManager.Key(Key.Down); InputManager.Key(Key.Down);
}); });
AddUntilStep("wait for not current", () => !songSelect.IsCurrentScreen()); AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
AddAssert("ensure selection didn't change", () => selected == Beatmap.Value); AddAssert("ensure selection didn't change", () => selected == Beatmap.Value);
} }
@ -202,23 +201,23 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault); AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault);
WorkingBeatmap selected = null; WorkingBeatmap? selected = null;
AddStep("store selected beatmap", () => selected = Beatmap.Value); AddStep("store selected beatmap", () => selected = Beatmap.Value);
AddUntilStep("wait for beatmaps to load", () => songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmap>().Any()); AddUntilStep("wait for beatmaps to load", () => songSelect!.Carousel.ChildrenOfType<DrawableCarouselBeatmap>().Any());
AddStep("select next and enter", () => AddStep("select next and enter", () =>
{ {
InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmap>() InputManager.MoveMouseTo(songSelect!.Carousel.ChildrenOfType<DrawableCarouselBeatmap>()
.First(b => !((CarouselBeatmap)b.Item).BeatmapInfo.Equals(songSelect.Carousel.SelectedBeatmapInfo))); .First(b => !((CarouselBeatmap)b.Item).BeatmapInfo.Equals(songSelect!.Carousel.SelectedBeatmapInfo)));
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
InputManager.Key(Key.Enter); InputManager.Key(Key.Enter);
}); });
AddUntilStep("wait for not current", () => !songSelect.IsCurrentScreen()); AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
AddAssert("ensure selection changed", () => selected != Beatmap.Value); AddAssert("ensure selection changed", () => selected != Beatmap.Value);
} }
@ -231,14 +230,14 @@ namespace osu.Game.Tests.Visual.SongSelect
waitForInitialSelection(); waitForInitialSelection();
WorkingBeatmap selected = null; WorkingBeatmap? selected = null;
AddStep("store selected beatmap", () => selected = Beatmap.Value); AddStep("store selected beatmap", () => selected = Beatmap.Value);
AddStep("select next and enter", () => AddStep("select next and enter", () =>
{ {
InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmap>() InputManager.MoveMouseTo(songSelect!.Carousel.ChildrenOfType<DrawableCarouselBeatmap>()
.First(b => !((CarouselBeatmap)b.Item).BeatmapInfo.Equals(songSelect.Carousel.SelectedBeatmapInfo))); .First(b => !((CarouselBeatmap)b.Item).BeatmapInfo.Equals(songSelect!.Carousel.SelectedBeatmapInfo)));
InputManager.PressButton(MouseButton.Left); InputManager.PressButton(MouseButton.Left);
@ -247,7 +246,7 @@ namespace osu.Game.Tests.Visual.SongSelect
InputManager.ReleaseButton(MouseButton.Left); InputManager.ReleaseButton(MouseButton.Left);
}); });
AddUntilStep("wait for not current", () => !songSelect.IsCurrentScreen()); AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
AddAssert("ensure selection didn't change", () => selected == Beatmap.Value); AddAssert("ensure selection didn't change", () => selected == Beatmap.Value);
} }
@ -260,11 +259,11 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect(); createSongSelect();
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child"))); AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
AddUntilStep("wait for not current", () => !songSelect.IsCurrentScreen()); AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
AddStep("return", () => songSelect.MakeCurrent()); AddStep("return", () => songSelect!.MakeCurrent());
AddUntilStep("wait for current", () => songSelect.IsCurrentScreen()); AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen());
AddAssert("filter count is 1", () => songSelect.FilterCount == 1); AddAssert("filter count is 1", () => songSelect!.FilterCount == 1);
} }
[Test] [Test]
@ -278,13 +277,13 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect(); createSongSelect();
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child"))); AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
AddUntilStep("wait for not current", () => !songSelect.IsCurrentScreen()); AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
AddStep("return", () => songSelect.MakeCurrent()); AddStep("return", () => songSelect!.MakeCurrent());
AddUntilStep("wait for current", () => songSelect.IsCurrentScreen()); AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen());
AddAssert("filter count is 2", () => songSelect.FilterCount == 2); AddAssert("filter count is 2", () => songSelect!.FilterCount == 2);
} }
[Test] [Test]
@ -295,7 +294,7 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect(); createSongSelect();
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child"))); AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
AddUntilStep("wait for not current", () => !songSelect.IsCurrentScreen()); AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
AddStep("update beatmap", () => AddStep("update beatmap", () =>
{ {
@ -304,9 +303,9 @@ namespace osu.Game.Tests.Visual.SongSelect
Beatmap.Value = manager.GetWorkingBeatmap(anotherBeatmap); Beatmap.Value = manager.GetWorkingBeatmap(anotherBeatmap);
}); });
AddStep("return", () => songSelect.MakeCurrent()); AddStep("return", () => songSelect!.MakeCurrent());
AddUntilStep("wait for current", () => songSelect.IsCurrentScreen()); AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen());
AddAssert("carousel updated", () => songSelect.Carousel.SelectedBeatmapInfo.Equals(Beatmap.Value.BeatmapInfo)); AddAssert("carousel updated", () => songSelect!.Carousel.SelectedBeatmapInfo?.Equals(Beatmap.Value.BeatmapInfo) == true);
} }
[Test] [Test]
@ -318,15 +317,15 @@ namespace osu.Game.Tests.Visual.SongSelect
addRulesetImportStep(0); addRulesetImportStep(0);
checkMusicPlaying(true); checkMusicPlaying(true);
AddStep("select first", () => songSelect.Carousel.SelectBeatmap(songSelect.Carousel.BeatmapSets.First().Beatmaps.First())); AddStep("select first", () => songSelect!.Carousel.SelectBeatmap(songSelect!.Carousel.BeatmapSets.First().Beatmaps.First()));
checkMusicPlaying(true); checkMusicPlaying(true);
AddStep("manual pause", () => music.TogglePause()); AddStep("manual pause", () => music.TogglePause());
checkMusicPlaying(false); checkMusicPlaying(false);
AddStep("select next difficulty", () => songSelect.Carousel.SelectNext(skipDifficulties: false)); AddStep("select next difficulty", () => songSelect!.Carousel.SelectNext(skipDifficulties: false));
checkMusicPlaying(false); checkMusicPlaying(false);
AddStep("select next set", () => songSelect.Carousel.SelectNext()); AddStep("select next set", () => songSelect!.Carousel.SelectNext());
checkMusicPlaying(true); checkMusicPlaying(true);
} }
@ -366,13 +365,13 @@ namespace osu.Game.Tests.Visual.SongSelect
public void TestDummy() public void TestDummy()
{ {
createSongSelect(); createSongSelect();
AddUntilStep("dummy selected", () => songSelect.CurrentBeatmap == defaultBeatmap); AddUntilStep("dummy selected", () => songSelect!.CurrentBeatmap == defaultBeatmap);
AddUntilStep("dummy shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap == defaultBeatmap); AddUntilStep("dummy shown on wedge", () => songSelect!.CurrentBeatmapDetailsBeatmap == defaultBeatmap);
addManyTestMaps(); addManyTestMaps();
AddUntilStep("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap); AddUntilStep("random map selected", () => songSelect!.CurrentBeatmap != defaultBeatmap);
} }
[Test] [Test]
@ -381,7 +380,7 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect(); createSongSelect();
addManyTestMaps(); addManyTestMaps();
AddUntilStep("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap); AddUntilStep("random map selected", () => songSelect!.CurrentBeatmap != defaultBeatmap);
AddStep(@"Sort by Artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist)); AddStep(@"Sort by Artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist));
AddStep(@"Sort by Title", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title)); AddStep(@"Sort by Title", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title));
@ -398,7 +397,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
createSongSelect(); createSongSelect();
addRulesetImportStep(2); addRulesetImportStep(2);
AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); AddUntilStep("no selection", () => songSelect!.Carousel.SelectedBeatmapInfo == null);
} }
[Test] [Test]
@ -408,13 +407,13 @@ namespace osu.Game.Tests.Visual.SongSelect
changeRuleset(2); changeRuleset(2);
addRulesetImportStep(2); addRulesetImportStep(2);
addRulesetImportStep(1); addRulesetImportStep(1);
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.Ruleset.OnlineID == 2); AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.Ruleset.OnlineID == 2);
changeRuleset(1); changeRuleset(1);
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.Ruleset.OnlineID == 1); AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.Ruleset.OnlineID == 1);
changeRuleset(0); changeRuleset(0);
AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); AddUntilStep("no selection", () => songSelect!.Carousel.SelectedBeatmapInfo == null);
} }
[Test] [Test]
@ -423,7 +422,7 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect(); createSongSelect();
changeRuleset(0); changeRuleset(0);
Live<BeatmapSetInfo> original = null!; Live<BeatmapSetInfo>? original = null;
int originalOnlineSetID = 0; int originalOnlineSetID = 0;
AddStep(@"Sort by artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist)); AddStep(@"Sort by artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist));
@ -431,12 +430,17 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("import original", () => AddStep("import original", () =>
{ {
original = manager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely(); original = manager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely();
originalOnlineSetID = original!.Value.OnlineID;
Debug.Assert(original != null);
originalOnlineSetID = original.Value.OnlineID;
}); });
// This will move the beatmap set to a different location in the carousel. // This will move the beatmap set to a different location in the carousel.
AddStep("Update original with bogus info", () => AddStep("Update original with bogus info", () =>
{ {
Debug.Assert(original != null);
original.PerformWrite(set => original.PerformWrite(set =>
{ {
foreach (var beatmap in set.Beatmaps) foreach (var beatmap in set.Beatmaps)
@ -457,13 +461,19 @@ namespace osu.Game.Tests.Visual.SongSelect
manager.Import(testBeatmapSetInfo); manager.Import(testBeatmapSetInfo);
}, 10); }, 10);
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID); AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID);
Task<Live<BeatmapSetInfo>> updateTask = null!; Task<Live<BeatmapSetInfo>?> updateTask = null!;
AddStep("update beatmap", () => updateTask = manager.ImportAsUpdate(new ProgressNotification(), new ImportTask(TestResources.GetQuickTestBeatmapForImport()), original.Value));
AddStep("update beatmap", () =>
{
Debug.Assert(original != null);
updateTask = manager.ImportAsUpdate(new ProgressNotification(), new ImportTask(TestResources.GetQuickTestBeatmapForImport()), original.Value);
});
AddUntilStep("wait for update completion", () => updateTask.IsCompleted); AddUntilStep("wait for update completion", () => updateTask.IsCompleted);
AddUntilStep("retained selection", () => songSelect.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID); AddUntilStep("retained selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID);
} }
[Test] [Test]
@ -473,13 +483,13 @@ namespace osu.Game.Tests.Visual.SongSelect
changeRuleset(2); changeRuleset(2);
addRulesetImportStep(2); addRulesetImportStep(2);
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.Ruleset.OnlineID == 2); AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.Ruleset.OnlineID == 2);
addRulesetImportStep(0); addRulesetImportStep(0);
addRulesetImportStep(0); addRulesetImportStep(0);
addRulesetImportStep(0); addRulesetImportStep(0);
BeatmapInfo target = null; BeatmapInfo? target = null;
AddStep("select beatmap/ruleset externally", () => AddStep("select beatmap/ruleset externally", () =>
{ {
@ -490,10 +500,10 @@ namespace osu.Game.Tests.Visual.SongSelect
Beatmap.Value = manager.GetWorkingBeatmap(target); Beatmap.Value = manager.GetWorkingBeatmap(target);
}); });
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.Equals(target)); AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.Equals(target) == true);
// this is an important check, to make sure updateComponentFromBeatmap() was actually run // this is an important check, to make sure updateComponentFromBeatmap() was actually run
AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo.MatchesOnlineID(target)); AddUntilStep("selection shown on wedge", () => songSelect!.CurrentBeatmapDetailsBeatmap.BeatmapInfo.MatchesOnlineID(target));
} }
[Test] [Test]
@ -503,13 +513,13 @@ namespace osu.Game.Tests.Visual.SongSelect
changeRuleset(2); changeRuleset(2);
addRulesetImportStep(2); addRulesetImportStep(2);
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.Ruleset.OnlineID == 2); AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.Ruleset.OnlineID == 2);
addRulesetImportStep(0); addRulesetImportStep(0);
addRulesetImportStep(0); addRulesetImportStep(0);
addRulesetImportStep(0); addRulesetImportStep(0);
BeatmapInfo target = null; BeatmapInfo? target = null;
AddStep("select beatmap/ruleset externally", () => AddStep("select beatmap/ruleset externally", () =>
{ {
@ -520,12 +530,12 @@ namespace osu.Game.Tests.Visual.SongSelect
Ruleset.Value = rulesets.AvailableRulesets.First(r => r.OnlineID == 0); Ruleset.Value = rulesets.AvailableRulesets.First(r => r.OnlineID == 0);
}); });
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.Equals(target)); AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.Equals(target) == true);
AddUntilStep("has correct ruleset", () => Ruleset.Value.OnlineID == 0); AddUntilStep("has correct ruleset", () => Ruleset.Value.OnlineID == 0);
// this is an important check, to make sure updateComponentFromBeatmap() was actually run // this is an important check, to make sure updateComponentFromBeatmap() was actually run
AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo.MatchesOnlineID(target)); AddUntilStep("selection shown on wedge", () => songSelect!.CurrentBeatmapDetailsBeatmap.BeatmapInfo.MatchesOnlineID(target));
} }
[Test] [Test]
@ -543,12 +553,12 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("change ruleset", () => AddStep("change ruleset", () =>
{ {
SelectedMods.ValueChanged += onModChange; SelectedMods.ValueChanged += onModChange;
songSelect.Ruleset.ValueChanged += onRulesetChange; songSelect!.Ruleset.ValueChanged += onRulesetChange;
Ruleset.Value = new TaikoRuleset().RulesetInfo; Ruleset.Value = new TaikoRuleset().RulesetInfo;
SelectedMods.ValueChanged -= onModChange; SelectedMods.ValueChanged -= onModChange;
songSelect.Ruleset.ValueChanged -= onRulesetChange; songSelect!.Ruleset.ValueChanged -= onRulesetChange;
}); });
AddAssert("mods changed before ruleset", () => modChangeIndex < rulesetChangeIndex); AddAssert("mods changed before ruleset", () => modChangeIndex < rulesetChangeIndex);
@ -579,18 +589,18 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
createSongSelect(); createSongSelect();
addManyTestMaps(); addManyTestMaps();
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo != null);
bool startRequested = false; bool startRequested = false;
AddStep("set filter and finalize", () => AddStep("set filter and finalize", () =>
{ {
songSelect.StartRequested = () => startRequested = true; songSelect!.StartRequested = () => startRequested = true;
songSelect.Carousel.Filter(new FilterCriteria { SearchText = "somestringthatshouldn'tbematchable" }); songSelect!.Carousel.Filter(new FilterCriteria { SearchText = "somestringthatshouldn'tbematchable" });
songSelect.FinaliseSelection(); songSelect!.FinaliseSelection();
songSelect.StartRequested = null; songSelect!.StartRequested = null;
}); });
AddAssert("start not requested", () => !startRequested); AddAssert("start not requested", () => !startRequested);
@ -610,15 +620,15 @@ namespace osu.Game.Tests.Visual.SongSelect
// used for filter check below // used for filter check below
AddStep("allow convert display", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); AddStep("allow convert display", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo != null);
AddStep("set filter text", () => songSelect.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = "nonono"); AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = "nonono");
AddUntilStep("dummy selected", () => Beatmap.Value is DummyWorkingBeatmap); AddUntilStep("dummy selected", () => Beatmap.Value is DummyWorkingBeatmap);
AddUntilStep("has no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); AddUntilStep("has no selection", () => songSelect!.Carousel.SelectedBeatmapInfo == null);
BeatmapInfo target = null; BeatmapInfo? target = null;
int targetRuleset = differentRuleset ? 1 : 0; int targetRuleset = differentRuleset ? 1 : 0;
@ -632,24 +642,24 @@ namespace osu.Game.Tests.Visual.SongSelect
Beatmap.Value = manager.GetWorkingBeatmap(target); Beatmap.Value = manager.GetWorkingBeatmap(target);
}); });
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo != null);
AddAssert("selected only shows expected ruleset (plus converts)", () => AddAssert("selected only shows expected ruleset (plus converts)", () =>
{ {
var selectedPanel = songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().First(s => s.Item.State.Value == CarouselItemState.Selected); var selectedPanel = songSelect!.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().First(s => s.Item.State.Value == CarouselItemState.Selected);
// special case for converts checked here. // special case for converts checked here.
return selectedPanel.ChildrenOfType<FilterableDifficultyIcon>().All(i => return selectedPanel.ChildrenOfType<FilterableDifficultyIcon>().All(i =>
i.IsFiltered || i.Item.BeatmapInfo.Ruleset.OnlineID == targetRuleset || i.Item.BeatmapInfo.Ruleset.OnlineID == 0); i.IsFiltered || i.Item.BeatmapInfo.Ruleset.OnlineID == targetRuleset || i.Item.BeatmapInfo.Ruleset.OnlineID == 0);
}); });
AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmapInfo?.MatchesOnlineID(target) == true); AddUntilStep("carousel has correct", () => songSelect!.Carousel.SelectedBeatmapInfo?.MatchesOnlineID(target) == true);
AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(target)); AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(target));
AddStep("reset filter text", () => songSelect.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = string.Empty); AddStep("reset filter text", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = string.Empty);
AddAssert("game still correct", () => Beatmap.Value?.BeatmapInfo.MatchesOnlineID(target) == true); AddAssert("game still correct", () => Beatmap.Value?.BeatmapInfo.MatchesOnlineID(target) == true);
AddAssert("carousel still correct", () => songSelect.Carousel.SelectedBeatmapInfo.MatchesOnlineID(target)); AddAssert("carousel still correct", () => songSelect!.Carousel.SelectedBeatmapInfo.MatchesOnlineID(target));
} }
[Test] [Test]
@ -662,15 +672,15 @@ namespace osu.Game.Tests.Visual.SongSelect
changeRuleset(0); changeRuleset(0);
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo != null);
AddStep("set filter text", () => songSelect.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = "nonono"); AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = "nonono");
AddUntilStep("dummy selected", () => Beatmap.Value is DummyWorkingBeatmap); AddUntilStep("dummy selected", () => Beatmap.Value is DummyWorkingBeatmap);
AddUntilStep("has no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); AddUntilStep("has no selection", () => songSelect!.Carousel.SelectedBeatmapInfo == null);
BeatmapInfo target = null; BeatmapInfo? target = null;
AddStep("select beatmap externally", () => AddStep("select beatmap externally", () =>
{ {
@ -682,15 +692,15 @@ namespace osu.Game.Tests.Visual.SongSelect
Beatmap.Value = manager.GetWorkingBeatmap(target); Beatmap.Value = manager.GetWorkingBeatmap(target);
}); });
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo != null);
AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmapInfo?.MatchesOnlineID(target) == true); AddUntilStep("carousel has correct", () => songSelect!.Carousel.SelectedBeatmapInfo?.MatchesOnlineID(target) == true);
AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(target)); AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(target));
AddStep("set filter text", () => songSelect.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = "nononoo"); AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = "nononoo");
AddUntilStep("game lost selection", () => Beatmap.Value is DummyWorkingBeatmap); AddUntilStep("game lost selection", () => Beatmap.Value is DummyWorkingBeatmap);
AddAssert("carousel lost selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); AddAssert("carousel lost selection", () => songSelect!.Carousel.SelectedBeatmapInfo == null);
} }
[Test] [Test]
@ -711,11 +721,11 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader); AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader);
AddAssert("autoplay selected", () => songSelect.Mods.Value.Single() is ModAutoplay); AddAssert("autoplay selected", () => songSelect!.Mods.Value.Single() is ModAutoplay);
AddUntilStep("wait for return to ss", () => songSelect.IsCurrentScreen()); AddUntilStep("wait for return to ss", () => songSelect!.IsCurrentScreen());
AddAssert("no mods selected", () => songSelect.Mods.Value.Count == 0); AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0);
} }
[Test] [Test]
@ -738,11 +748,11 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader); AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader);
AddAssert("autoplay selected", () => songSelect.Mods.Value.Single() is ModAutoplay); AddAssert("autoplay selected", () => songSelect!.Mods.Value.Single() is ModAutoplay);
AddUntilStep("wait for return to ss", () => songSelect.IsCurrentScreen()); AddUntilStep("wait for return to ss", () => songSelect!.IsCurrentScreen());
AddAssert("autoplay still selected", () => songSelect.Mods.Value.Single() is ModAutoplay); AddAssert("autoplay still selected", () => songSelect!.Mods.Value.Single() is ModAutoplay);
} }
[Test] [Test]
@ -765,11 +775,11 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader); AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader);
AddAssert("only autoplay selected", () => songSelect.Mods.Value.Single() is ModAutoplay); AddAssert("only autoplay selected", () => songSelect!.Mods.Value.Single() is ModAutoplay);
AddUntilStep("wait for return to ss", () => songSelect.IsCurrentScreen()); AddUntilStep("wait for return to ss", () => songSelect!.IsCurrentScreen());
AddAssert("relax returned", () => songSelect.Mods.Value.Single() is ModRelax); AddAssert("relax returned", () => songSelect!.Mods.Value.Single() is ModRelax);
} }
[Test] [Test]
@ -778,10 +788,10 @@ namespace osu.Game.Tests.Visual.SongSelect
Guid? previousID = null; Guid? previousID = null;
createSongSelect(); createSongSelect();
addRulesetImportStep(0); addRulesetImportStep(0);
AddStep("Move to last difficulty", () => songSelect.Carousel.SelectBeatmap(songSelect.Carousel.BeatmapSets.First().Beatmaps.Last())); AddStep("Move to last difficulty", () => songSelect!.Carousel.SelectBeatmap(songSelect!.Carousel.BeatmapSets.First().Beatmaps.Last()));
AddStep("Store current ID", () => previousID = songSelect.Carousel.SelectedBeatmapInfo.ID); AddStep("Store current ID", () => previousID = songSelect!.Carousel.SelectedBeatmapInfo!.ID);
AddStep("Hide first beatmap", () => manager.Hide(songSelect.Carousel.SelectedBeatmapSet.Beatmaps.First())); AddStep("Hide first beatmap", () => manager.Hide(songSelect!.Carousel.SelectedBeatmapSet!.Beatmaps.First()));
AddAssert("Selected beatmap has not changed", () => songSelect.Carousel.SelectedBeatmapInfo.ID == previousID); AddAssert("Selected beatmap has not changed", () => songSelect!.Carousel.SelectedBeatmapInfo?.ID == previousID);
} }
[Test] [Test]
@ -792,17 +802,24 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for selection", () => !Beatmap.IsDefault); AddUntilStep("wait for selection", () => !Beatmap.IsDefault);
DrawableCarouselBeatmapSet set = null; DrawableCarouselBeatmapSet set = null!;
AddStep("Find the DrawableCarouselBeatmapSet", () => AddStep("Find the DrawableCarouselBeatmapSet", () =>
{ {
set = songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().First(); set = songSelect!.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().First();
}); });
FilterableDifficultyIcon difficultyIcon = null; FilterableDifficultyIcon difficultyIcon = null!;
AddUntilStep("Find an icon", () => AddUntilStep("Find an icon", () =>
{ {
return (difficultyIcon = set.ChildrenOfType<FilterableDifficultyIcon>() var foundIcon = set.ChildrenOfType<FilterableDifficultyIcon>()
.FirstOrDefault(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex())) != null; .FirstOrDefault(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex());
if (foundIcon == null)
return false;
difficultyIcon = foundIcon;
return true;
}); });
AddStep("Click on a difficulty", () => AddStep("Click on a difficulty", () =>
@ -815,21 +832,24 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("Selected beatmap correct", () => getCurrentBeatmapIndex() == getDifficultyIconIndex(set, difficultyIcon)); AddAssert("Selected beatmap correct", () => getCurrentBeatmapIndex() == getDifficultyIconIndex(set, difficultyIcon));
double? maxBPM = null; double? maxBPM = null;
AddStep("Filter some difficulties", () => songSelect.Carousel.Filter(new FilterCriteria AddStep("Filter some difficulties", () => songSelect!.Carousel.Filter(new FilterCriteria
{ {
BPM = new FilterCriteria.OptionalRange<double> BPM = new FilterCriteria.OptionalRange<double>
{ {
Min = maxBPM = songSelect.Carousel.SelectedBeatmapSet.MaxBPM, Min = maxBPM = songSelect!.Carousel.SelectedBeatmapSet!.MaxBPM,
IsLowerInclusive = true IsLowerInclusive = true
} }
})); }));
BeatmapInfo filteredBeatmap = null; BeatmapInfo? filteredBeatmap = null;
FilterableDifficultyIcon filteredIcon = null; FilterableDifficultyIcon? filteredIcon = null;
AddStep("Get filtered icon", () => AddStep("Get filtered icon", () =>
{ {
var selectedSet = songSelect.Carousel.SelectedBeatmapSet; var selectedSet = songSelect!.Carousel.SelectedBeatmapSet;
Debug.Assert(selectedSet != null);
filteredBeatmap = selectedSet.Beatmaps.First(b => b.BPM < maxBPM); filteredBeatmap = selectedSet.Beatmaps.First(b => b.BPM < maxBPM);
int filteredBeatmapIndex = getBeatmapIndex(selectedSet, filteredBeatmap); int filteredBeatmapIndex = getBeatmapIndex(selectedSet, filteredBeatmap);
filteredIcon = set.ChildrenOfType<FilterableDifficultyIcon>().ElementAt(filteredBeatmapIndex); filteredIcon = set.ChildrenOfType<FilterableDifficultyIcon>().ElementAt(filteredBeatmapIndex);
@ -842,7 +862,7 @@ namespace osu.Game.Tests.Visual.SongSelect
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmapInfo.Equals(filteredBeatmap)); AddAssert("Selected beatmap correct", () => songSelect!.Carousel.SelectedBeatmapInfo?.Equals(filteredBeatmap) == true);
} }
[Test] [Test]
@ -907,14 +927,14 @@ namespace osu.Game.Tests.Visual.SongSelect
manager.Import(TestResources.CreateTestBeatmapSetInfo(3, usableRulesets)); manager.Import(TestResources.CreateTestBeatmapSetInfo(3, usableRulesets));
}); });
DrawableCarouselBeatmapSet set = null; DrawableCarouselBeatmapSet? set = null;
AddUntilStep("Find the DrawableCarouselBeatmapSet", () => AddUntilStep("Find the DrawableCarouselBeatmapSet", () =>
{ {
set = songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().FirstOrDefault(); set = songSelect!.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().FirstOrDefault();
return set != null; return set != null;
}); });
FilterableDifficultyIcon difficultyIcon = null; FilterableDifficultyIcon? difficultyIcon = null;
AddUntilStep("Find an icon for different ruleset", () => AddUntilStep("Find an icon for different ruleset", () =>
{ {
difficultyIcon = set.ChildrenOfType<FilterableDifficultyIcon>() difficultyIcon = set.ChildrenOfType<FilterableDifficultyIcon>()
@ -937,7 +957,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.OnlineID == 3); AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.OnlineID == 3);
AddAssert("Selected beatmap still same set", () => songSelect.Carousel.SelectedBeatmapInfo.BeatmapSet?.OnlineID == previousSetID); AddAssert("Selected beatmap still same set", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == previousSetID);
AddAssert("Selected beatmap is mania", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID == 3); AddAssert("Selected beatmap is mania", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID == 3);
} }
@ -948,7 +968,7 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect(); createSongSelect();
BeatmapSetInfo imported = null; BeatmapSetInfo? imported = null;
AddStep("import huge difficulty count map", () => AddStep("import huge difficulty count map", () =>
{ {
@ -956,20 +976,27 @@ namespace osu.Game.Tests.Visual.SongSelect
imported = manager.Import(TestResources.CreateTestBeatmapSetInfo(50, usableRulesets))?.Value; imported = manager.Import(TestResources.CreateTestBeatmapSetInfo(50, usableRulesets))?.Value;
}); });
AddStep("select the first beatmap of import", () => Beatmap.Value = manager.GetWorkingBeatmap(imported.Beatmaps.First())); AddStep("select the first beatmap of import", () => Beatmap.Value = manager.GetWorkingBeatmap(imported?.Beatmaps.First()));
DrawableCarouselBeatmapSet set = null; DrawableCarouselBeatmapSet? set = null;
AddUntilStep("Find the DrawableCarouselBeatmapSet", () => AddUntilStep("Find the DrawableCarouselBeatmapSet", () =>
{ {
set = songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().FirstOrDefault(); set = songSelect!.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().FirstOrDefault();
return set != null; return set != null;
}); });
GroupedDifficultyIcon groupIcon = null; GroupedDifficultyIcon groupIcon = null!;
AddUntilStep("Find group icon for different ruleset", () => AddUntilStep("Find group icon for different ruleset", () =>
{ {
return (groupIcon = set.ChildrenOfType<GroupedDifficultyIcon>() var foundIcon = set.ChildrenOfType<GroupedDifficultyIcon>()
.FirstOrDefault(icon => icon.Items.First().BeatmapInfo.Ruleset.OnlineID == 3)) != null; .FirstOrDefault(icon => icon.Items.First().BeatmapInfo.Ruleset.OnlineID == 3);
if (foundIcon == null)
return false;
groupIcon = foundIcon;
return true;
}); });
AddAssert("Check ruleset is osu!", () => Ruleset.Value.OnlineID == 0); AddAssert("Check ruleset is osu!", () => Ruleset.Value.OnlineID == 0);
@ -1004,7 +1031,7 @@ namespace osu.Game.Tests.Visual.SongSelect
// this ruleset change should be overridden by the present. // this ruleset change should be overridden by the present.
Ruleset.Value = getSwitchBeatmap().Ruleset; Ruleset.Value = getSwitchBeatmap().Ruleset;
songSelect.PresentScore(new ScoreInfo songSelect!.PresentScore(new ScoreInfo
{ {
User = new APIUser { Username = "woo" }, User = new APIUser { Username = "woo" },
BeatmapInfo = getPresentBeatmap(), BeatmapInfo = getPresentBeatmap(),
@ -1012,7 +1039,7 @@ namespace osu.Game.Tests.Visual.SongSelect
}); });
}); });
AddUntilStep("wait for results screen presented", () => !songSelect.IsCurrentScreen()); AddUntilStep("wait for results screen presented", () => !songSelect!.IsCurrentScreen());
AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(getPresentBeatmap())); AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(getPresentBeatmap()));
AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0); AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0);
@ -1038,10 +1065,10 @@ namespace osu.Game.Tests.Visual.SongSelect
// this beatmap change should be overridden by the present. // this beatmap change should be overridden by the present.
Beatmap.Value = manager.GetWorkingBeatmap(getSwitchBeatmap()); Beatmap.Value = manager.GetWorkingBeatmap(getSwitchBeatmap());
songSelect.PresentScore(TestResources.CreateTestScoreInfo(getPresentBeatmap())); songSelect!.PresentScore(TestResources.CreateTestScoreInfo(getPresentBeatmap()));
}); });
AddUntilStep("wait for results screen presented", () => !songSelect.IsCurrentScreen()); AddUntilStep("wait for results screen presented", () => !songSelect!.IsCurrentScreen());
AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(getPresentBeatmap())); AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(getPresentBeatmap()));
AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0); AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0);
@ -1054,23 +1081,29 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect(); createSongSelect();
AddStep("toggle mod overlay on", () => InputManager.Key(Key.F1)); AddStep("toggle mod overlay on", () => InputManager.Key(Key.F1));
AddUntilStep("mod overlay shown", () => songSelect.ModSelect.State.Value == Visibility.Visible); AddUntilStep("mod overlay shown", () => songSelect!.ModSelect.State.Value == Visibility.Visible);
AddStep("toggle mod overlay off", () => InputManager.Key(Key.F1)); AddStep("toggle mod overlay off", () => InputManager.Key(Key.F1));
AddUntilStep("mod overlay hidden", () => songSelect.ModSelect.State.Value == Visibility.Hidden); AddUntilStep("mod overlay hidden", () => songSelect!.ModSelect.State.Value == Visibility.Hidden);
} }
private void waitForInitialSelection() private void waitForInitialSelection()
{ {
AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault); AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault);
AddUntilStep("wait for difficulty panels visible", () => songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmap>().Any()); AddUntilStep("wait for difficulty panels visible", () => songSelect!.Carousel.ChildrenOfType<DrawableCarouselBeatmap>().Any());
} }
private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.IndexOf(info); private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.IndexOf(info);
private NoResultsPlaceholder getPlaceholder() => songSelect.ChildrenOfType<NoResultsPlaceholder>().FirstOrDefault(); private NoResultsPlaceholder? getPlaceholder() => songSelect!.ChildrenOfType<NoResultsPlaceholder>().FirstOrDefault();
private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmapInfo); private int getCurrentBeatmapIndex()
{
Debug.Assert(songSelect!.Carousel.SelectedBeatmapSet != null);
Debug.Assert(songSelect!.Carousel.SelectedBeatmapInfo != null);
return getBeatmapIndex(songSelect!.Carousel.SelectedBeatmapSet, songSelect!.Carousel.SelectedBeatmapInfo);
}
private int getDifficultyIconIndex(DrawableCarouselBeatmapSet set, FilterableDifficultyIcon icon) private int getDifficultyIconIndex(DrawableCarouselBeatmapSet set, FilterableDifficultyIcon icon)
{ {
@ -1079,14 +1112,14 @@ namespace osu.Game.Tests.Visual.SongSelect
private void addRulesetImportStep(int id) private void addRulesetImportStep(int id)
{ {
Live<BeatmapSetInfo> imported = null; Live<BeatmapSetInfo>? imported = null;
AddStep($"import test map for ruleset {id}", () => imported = importForRuleset(id)); AddStep($"import test map for ruleset {id}", () => imported = importForRuleset(id));
// This is specifically for cases where the add is happening post song select load. // This is specifically for cases where the add is happening post song select load.
// For cases where song select is null, the assertions are provided by the load checks. // For cases where song select is null, the assertions are provided by the load checks.
AddUntilStep("wait for imported to arrive in carousel", () => songSelect == null || songSelect.Carousel.BeatmapSets.Any(s => s.ID == imported?.ID)); AddUntilStep("wait for imported to arrive in carousel", () => songSelect == null || songSelect!.Carousel.BeatmapSets.Any(s => s.ID == imported?.ID));
} }
private Live<BeatmapSetInfo> importForRuleset(int id) => manager.Import(TestResources.CreateTestBeatmapSetInfo(3, rulesets.AvailableRulesets.Where(r => r.OnlineID == id).ToArray())); private Live<BeatmapSetInfo>? importForRuleset(int id) => manager.Import(TestResources.CreateTestBeatmapSetInfo(3, rulesets.AvailableRulesets.Where(r => r.OnlineID == id).ToArray()));
private void checkMusicPlaying(bool playing) => private void checkMusicPlaying(bool playing) =>
AddUntilStep($"music {(playing ? "" : "not ")}playing", () => music.IsPlaying == playing); AddUntilStep($"music {(playing ? "" : "not ")}playing", () => music.IsPlaying == playing);
@ -1098,8 +1131,8 @@ namespace osu.Game.Tests.Visual.SongSelect
private void createSongSelect() private void createSongSelect()
{ {
AddStep("create song select", () => LoadScreen(songSelect = new TestSongSelect())); AddStep("create song select", () => LoadScreen(songSelect = new TestSongSelect()));
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen()); AddUntilStep("wait for present", () => songSelect!.IsCurrentScreen());
AddUntilStep("wait for carousel loaded", () => songSelect.Carousel.IsAlive); AddUntilStep("wait for carousel loaded", () => songSelect!.Carousel.IsAlive);
} }
/// <summary> /// <summary>
@ -1123,12 +1156,14 @@ namespace osu.Game.Tests.Visual.SongSelect
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
rulesets?.Dispose();
if (rulesets.IsNotNull())
rulesets.Dispose();
} }
private class TestSongSelect : PlaySongSelect private class TestSongSelect : PlaySongSelect
{ {
public Action StartRequested; public Action? StartRequested;
public new Bindable<RulesetInfo> Ruleset => base.Ruleset; public new Bindable<RulesetInfo> Ruleset => base.Ruleset;

View File

@ -0,0 +1,94 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Globalization;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneSizePreservingSpriteText : OsuGridTestScene
{
private readonly List<Container> parentContainers = new List<Container>();
private readonly List<UprightAspectMaintainingContainer> childContainers = new List<UprightAspectMaintainingContainer>();
private readonly OsuSpriteText osuSpriteText = new OsuSpriteText();
private readonly SizePreservingSpriteText sizePreservingSpriteText = new SizePreservingSpriteText();
public TestSceneSizePreservingSpriteText()
: base(1, 2)
{
for (int i = 0; i < 2; i++)
{
UprightAspectMaintainingContainer childContainer;
Container parentContainer = new Container
{
Origin = Anchor.BottomRight,
Anchor = Anchor.BottomCentre,
AutoSizeAxes = Axes.Both,
Rotation = 45,
Y = -200,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.Red,
},
childContainer = new UprightAspectMaintainingContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.Blue,
},
}
},
}
};
Container cellInfo = new Container
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Margin = new MarginPadding
{
Top = 100,
},
Child = new OsuSpriteText
{
Text = (i == 0) ? "OsuSpriteText" : "SizePreservingSpriteText",
Font = OsuFont.GetFont(Typeface.Inter, weight: FontWeight.Bold, size: 40),
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
},
};
parentContainers.Add(parentContainer);
childContainers.Add(childContainer);
Cell(i).Add(cellInfo);
Cell(i).Add(parentContainer);
}
childContainers[0].Add(osuSpriteText);
childContainers[1].Add(sizePreservingSpriteText);
osuSpriteText.Font = sizePreservingSpriteText.Font = OsuFont.GetFont(Typeface.Venera, weight: FontWeight.Bold, size: 20);
}
protected override void Update()
{
base.Update();
osuSpriteText.Text = sizePreservingSpriteText.Text = DateTime.Now.ToString(CultureInfo.InvariantCulture);
}
}
}

View File

@ -0,0 +1,244 @@
// 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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics;
using osuTK.Graphics;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneUprightAspectMaintainingContainer : OsuGridTestScene
{
private const int rows = 3;
private const int columns = 4;
private readonly ScaleMode[] scaleModeValues = { ScaleMode.NoScaling, ScaleMode.Horizontal, ScaleMode.Vertical };
private readonly float[] scalingFactorValues = { 1.0f / 3, 1.0f / 2, 1.0f, 1.5f };
private readonly List<List<Container>> parentContainers = new List<List<Container>>(rows);
private readonly List<List<UprightAspectMaintainingContainer>> childContainers = new List<List<UprightAspectMaintainingContainer>>(rows);
// Preferably should be set to (4 * 2^n)
private const int rotation_step_count = 3;
private readonly List<int> flipStates = new List<int>();
private readonly List<float> rotationSteps = new List<float>();
private readonly List<float> scaleSteps = new List<float>();
public TestSceneUprightAspectMaintainingContainer()
: base(rows, columns)
{
for (int i = 0; i < rows; i++)
{
parentContainers.Add(new List<Container>());
childContainers.Add(new List<UprightAspectMaintainingContainer>());
for (int j = 0; j < columns; j++)
{
UprightAspectMaintainingContainer child;
Container parent = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Height = 80,
Width = 80,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = new Color4(255, 0, 0, 160),
},
new OsuSpriteText
{
Text = "Parent",
},
child = new UprightAspectMaintainingContainer
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
AutoSizeAxes = Axes.Both,
// These are the parameters being Tested
Scaling = scaleModeValues[i],
ScalingFactor = scalingFactorValues[j],
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = new Color4(0, 0, 255, 160),
},
new OsuSpriteText
{
Text = "Text",
Font = OsuFont.Numeric,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Padding = new MarginPadding
{
Horizontal = 4,
Vertical = 4,
}
},
}
}
}
};
Container cellInfo = new Container
{
Children = new Drawable[]
{
new OsuSpriteText
{
Text = "Scaling: " + scaleModeValues[i].ToString(),
},
new OsuSpriteText
{
Text = "ScalingFactor: " + scalingFactorValues[j].ToString("0.00"),
Margin = new MarginPadding
{
Top = 15,
},
},
},
};
Cell(i * columns + j).Add(cellInfo);
Cell(i * columns + j).Add(parent);
parentContainers[i].Add(parent);
childContainers[i].Add(child);
}
}
flipStates.AddRange(new[] { 1, -1 });
rotationSteps.AddRange(Enumerable.Range(0, rotation_step_count).Select(x => 360f * ((float)x / rotation_step_count)));
scaleSteps.AddRange(new[] { 1, 0.3f, 1.5f });
}
[Test]
public void ExplicitlySizedParent()
{
var parentStates = from xFlip in flipStates
from yFlip in flipStates
from xScale in scaleSteps
from yScale in scaleSteps
from rotation in rotationSteps
select new { xFlip, yFlip, xScale, yScale, rotation };
foreach (var state in parentStates)
{
Vector2 parentScale = new Vector2(state.xFlip * state.xScale, state.yFlip * state.yScale);
float parentRotation = state.rotation;
AddStep("S: (" + parentScale.X.ToString("0.00") + ", " + parentScale.Y.ToString("0.00") + "), R: " + parentRotation.ToString("0.00"), () =>
{
foreach (List<Container> list in parentContainers)
{
foreach (Container container in list)
{
container.Scale = parentScale;
container.Rotation = parentRotation;
}
}
});
AddAssert("Check if state is valid", () =>
{
foreach (int i in Enumerable.Range(0, parentContainers.Count))
{
foreach (int j in Enumerable.Range(0, parentContainers[i].Count))
{
if (!uprightAspectMaintainingContainerStateIsValid(parentContainers[i][j], childContainers[i][j]))
return false;
}
}
return true;
});
}
}
private bool uprightAspectMaintainingContainerStateIsValid(Container parent, UprightAspectMaintainingContainer child)
{
Matrix3 parentMatrix = parent.DrawInfo.Matrix;
Matrix3 childMatrix = child.DrawInfo.Matrix;
Vector3 childScale = childMatrix.ExtractScale();
Vector3 parentScale = parentMatrix.ExtractScale();
// Orientation check
if (!(isNearlyZero(MathF.Abs(childMatrix.M21)) && isNearlyZero(MathF.Abs(childMatrix.M12))))
return false;
// flip check
if (!(childMatrix.M11 * childMatrix.M22 > 0))
return false;
// Aspect ratio check
if (!isNearlyZero(childScale.X - childScale.Y))
return false;
// ScalingMode check
switch (child.Scaling)
{
case ScaleMode.NoScaling:
if (!(isNearlyZero(childMatrix.M11 - 1.0f) && isNearlyZero(childMatrix.M22 - 1.0f)))
return false;
break;
case ScaleMode.Vertical:
if (!(checkScaling(child.ScalingFactor, parentScale.Y, childScale.Y)))
return false;
break;
case ScaleMode.Horizontal:
if (!(checkScaling(child.ScalingFactor, parentScale.X, childScale.X)))
return false;
break;
}
return true;
}
private bool checkScaling(float scalingFactor, float parentScale, float childScale)
{
if (scalingFactor <= 1.0f)
{
if (!isNearlyZero(1.0f + (parentScale - 1.0f) * scalingFactor - childScale))
return false;
}
else if (scalingFactor > 1.0f)
{
if (parentScale < 1.0f)
{
if (!isNearlyZero((parentScale * (1.0f / scalingFactor)) - childScale))
return false;
}
else if (!isNearlyZero(parentScale * scalingFactor - childScale))
return false;
}
return true;
}
private bool isNearlyZero(float f, float epsilon = Precision.FLOAT_EPSILON)
{
return f < epsilon;
}
}
}

View File

@ -106,13 +106,16 @@ namespace osu.Game.Tournament.Models
} }
/// <summary> /// <summary>
/// Initialise this match with zeroed scores. Will be a noop if either team is not present. /// Initialise this match with zeroed scores. Will be a noop if either team is not present or if either of the scores are non-zero.
/// </summary> /// </summary>
public void StartMatch() public void StartMatch()
{ {
if (Team1.Value == null || Team2.Value == null) if (Team1.Value == null || Team2.Value == null)
return; return;
if (Team1Score.Value > 0 || Team2Score.Value > 0)
return;
Team1Score.Value = 0; Team1Score.Value = 0;
Team2Score.Value = 0; Team2Score.Value = 0;
} }

View File

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -14,6 +15,7 @@ using osu.Framework.Logging;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
namespace osu.Game namespace osu.Game
@ -23,6 +25,9 @@ namespace osu.Game
[Resolved] [Resolved]
private RulesetStore rulesetStore { get; set; } = null!; private RulesetStore rulesetStore { get; set; } = null!;
[Resolved]
private ScoreManager scoreManager { get; set; } = null!;
[Resolved] [Resolved]
private RealmAccess realmAccess { get; set; } = null!; private RealmAccess realmAccess { get; set; } = null!;
@ -46,6 +51,7 @@ namespace osu.Game
Logger.Log("Beginning background beatmap processing.."); Logger.Log("Beginning background beatmap processing..");
checkForOutdatedStarRatings(); checkForOutdatedStarRatings();
processBeatmapSetsWithMissingMetrics(); processBeatmapSetsWithMissingMetrics();
processScoresWithMissingStatistics();
}).ContinueWith(t => }).ContinueWith(t =>
{ {
if (t.Exception?.InnerException is ObjectDisposedException) if (t.Exception?.InnerException is ObjectDisposedException)
@ -140,5 +146,52 @@ namespace osu.Game
}); });
} }
} }
private void processScoresWithMissingStatistics()
{
HashSet<Guid> scoreIds = new HashSet<Guid>();
Logger.Log("Querying for scores to reprocess...");
realmAccess.Run(r =>
{
foreach (var score in r.All<ScoreInfo>())
{
if (score.Statistics.Sum(kvp => kvp.Value) > 0 && score.MaximumStatistics.Sum(kvp => kvp.Value) == 0)
scoreIds.Add(score.ID);
}
});
Logger.Log($"Found {scoreIds.Count} scores which require reprocessing.");
foreach (var id in scoreIds)
{
while (localUserPlayInfo?.IsPlaying.Value == true)
{
Logger.Log("Background processing sleeping due to active gameplay...");
Thread.Sleep(TimeToSleepDuringGameplay);
}
try
{
var score = scoreManager.Query(s => s.ID == id);
scoreManager.PopulateMaximumStatistics(score);
// Can't use async overload because we're not on the update thread.
// ReSharper disable once MethodHasAsyncOverload
realmAccess.Write(r =>
{
r.Find<ScoreInfo>(id).MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics);
});
Logger.Log($"Populated maximum statistics for score {id}");
}
catch (Exception e)
{
Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}");
}
}
}
} }
} }

View File

@ -17,8 +17,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
{ {
public class DownloadButton : BeatmapCardIconButton public class DownloadButton : BeatmapCardIconButton
{ {
public IBindable<DownloadState> State => state; public Bindable<DownloadState> State { get; } = new Bindable<DownloadState>();
private readonly Bindable<DownloadState> state = new Bindable<DownloadState>();
private readonly APIBeatmapSet beatmapSet; private readonly APIBeatmapSet beatmapSet;
@ -48,14 +47,19 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
{ {
base.LoadComplete(); base.LoadComplete();
preferNoVideo.BindValueChanged(_ => updateState()); preferNoVideo.BindValueChanged(_ => updateState());
state.BindValueChanged(_ => updateState(), true); State.BindValueChanged(_ => updateState(), true);
FinishTransforms(true); FinishTransforms(true);
} }
private void updateState() private void updateState()
{ {
switch (state.Value) switch (State.Value)
{ {
case DownloadState.Unknown:
Action = null;
TooltipText = string.Empty;
break;
case DownloadState.Downloading: case DownloadState.Downloading:
case DownloadState.Importing: case DownloadState.Importing:
Action = null; Action = null;

View File

@ -41,6 +41,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
Anchor = Origin = Anchor.Centre; Anchor = Origin = Anchor.Centre;
// needed for touch input to work when card is not hovered/expanded
AlwaysPresent = true;
Children = new Drawable[] Children = new Drawable[]
{ {
icon = new SpriteIcon icon = new SpriteIcon

View File

@ -121,7 +121,18 @@ namespace osu.Game.Beatmaps
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
finalClockSource.ProcessFrame();
if (Source != null && Source is not IAdjustableClock && Source.CurrentTime < decoupledClock.CurrentTime)
{
// InterpolatingFramedClock won't interpolate backwards unless its source has an ElapsedFrameTime.
// See https://github.com/ppy/osu-framework/blob/ba1385330cc501f34937e08257e586c84e35d772/osu.Framework/Timing/InterpolatingFramedClock.cs#L91-L93
// This is not always the case here when doing large seeks.
// (Of note, this is not an issue if the source is adjustable, as the source is seeked to be in time by DecoupleableInterpolatingFramedClock).
// Rather than trying to get around this by fixing the framework clock stack, let's work around it for now.
Seek(Source.CurrentTime);
}
else
finalClockSource.ProcessFrame();
} }
public double TotalAppliedOffset public double TotalAppliedOffset

View File

@ -0,0 +1,119 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Layout;
using osuTK;
namespace osu.Game.Graphics.Containers
{
/// <summary>
/// A container that reverts any rotation (and optionally scale) applied by its direct parent.
/// </summary>
public class UprightAspectMaintainingContainer : Container
{
/// <summary>
/// Controls how much this container scales compared to its parent (default is 1.0f).
/// </summary>
public float ScalingFactor { get; set; } = 1;
/// <summary>
/// Controls the scaling of this container.
/// </summary>
public ScaleMode Scaling { get; set; } = ScaleMode.Vertical;
private readonly LayoutValue layout = new LayoutValue(Invalidation.DrawInfo, InvalidationSource.Parent);
public UprightAspectMaintainingContainer()
{
AddLayout(layout);
}
protected override void Update()
{
base.Update();
if (!layout.IsValid)
{
keepUprightAndUnstretched();
layout.Validate();
}
}
/// <summary>
/// Keeps the drawable upright and unstretched preventing it from being rotated, sheared, scaled or flipped with its Parent.
/// </summary>
private void keepUprightAndUnstretched()
{
// Decomposes the inverse of the parent DrawInfo.Matrix into rotation, shear and scale.
var parentMatrix = Parent.DrawInfo.Matrix;
// Remove Translation.>
parentMatrix.M31 = 0.0f;
parentMatrix.M32 = 0.0f;
Matrix3 reversedParent = parentMatrix.Inverted();
// Extract the rotation.
float angle = MathF.Atan2(reversedParent.M12, reversedParent.M11);
Rotation = MathHelper.RadiansToDegrees(angle);
// Remove rotation from the C matrix so that it only contains shear and scale.
Matrix3 m = Matrix3.CreateRotationZ(-angle);
reversedParent *= m;
// Extract shear.
float alpha = reversedParent.M21 / reversedParent.M22;
Shear = new Vector2(-alpha, 0);
// Etract scale.
float sx = reversedParent.M11;
float sy = reversedParent.M22;
Vector3 parentScale = parentMatrix.ExtractScale();
float usedScale = 1.0f;
switch (Scaling)
{
case ScaleMode.Horizontal:
usedScale = parentScale.X;
break;
case ScaleMode.Vertical:
usedScale = parentScale.Y;
break;
}
if (Scaling != ScaleMode.NoScaling)
{
if (ScalingFactor < 1.0f)
usedScale = 1.0f + (usedScale - 1.0f) * ScalingFactor;
if (ScalingFactor > 1.0f)
usedScale = (usedScale < 1.0f) ? usedScale * (1.0f / ScalingFactor) : usedScale * ScalingFactor;
}
Scale = new Vector2(sx * usedScale, sy * usedScale);
}
}
public enum ScaleMode
{
/// <summary>
/// Prevent this container from scaling.
/// </summary>
NoScaling,
/// <summary>
/// Scale uniformly (maintaining aspect ratio) based on the vertical scale of the parent.
/// </summary>
Vertical,
/// <summary>
/// Scale uniformly (maintaining aspect ratio) based on the horizontal scale of the parent.
/// </summary>
Horizontal,
}
}

View File

@ -102,26 +102,31 @@ namespace osu.Game.Graphics
/// <summary> /// <summary>
/// Retrieves the colour for a <see cref="HitResult"/>. /// Retrieves the colour for a <see cref="HitResult"/>.
/// </summary> /// </summary>
public Color4 ForHitResult(HitResult judgement) public Color4 ForHitResult(HitResult result)
{ {
switch (judgement) switch (result)
{ {
case HitResult.Perfect: case HitResult.SmallTickMiss:
case HitResult.Great: case HitResult.LargeTickMiss:
return Blue; case HitResult.Miss:
return Red;
case HitResult.Ok:
case HitResult.Good:
return Green;
case HitResult.Meh: case HitResult.Meh:
return Yellow; return Yellow;
case HitResult.Miss: case HitResult.Ok:
return Red; return Green;
case HitResult.Good:
return GreenLight;
case HitResult.SmallTickHit:
case HitResult.LargeTickHit:
case HitResult.Great:
return Blue;
default: default:
return Color4.White; return BlueLight;
} }
} }

View File

@ -0,0 +1,108 @@
// 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.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.Sprites
{
/// <summary>
/// A wrapped version of <see cref="OsuSpriteText"/> which will expand in size based on text content, but never shrink back down.
/// </summary>
public class SizePreservingSpriteText : CompositeDrawable
{
private readonly OsuSpriteText text = new OsuSpriteText();
private Vector2 maximumSize;
public SizePreservingSpriteText(Vector2? minimumSize = null)
{
text.Origin = Anchor.Centre;
text.Anchor = Anchor.Centre;
AddInternal(text);
maximumSize = minimumSize ?? Vector2.Zero;
}
protected override void Update()
{
Width = maximumSize.X = MathF.Max(maximumSize.X, text.Width);
Height = maximumSize.Y = MathF.Max(maximumSize.Y, text.Height);
}
public new Axes AutoSizeAxes
{
get => Axes.None;
set => throw new InvalidOperationException("You can't set AutoSizeAxes of this container");
}
/// <summary>
/// Gets or sets the text to be displayed.
/// </summary>
public LocalisableString Text
{
get => text.Text;
set => text.Text = value;
}
/// <summary>
/// Contains information on the font used to display the text.
/// </summary>
public FontUsage Font
{
get => text.Font;
set => text.Font = value;
}
/// <summary>
/// True if a shadow should be displayed around the text.
/// </summary>
public bool Shadow
{
get => text.Shadow;
set => text.Shadow = value;
}
/// <summary>
/// The colour of the shadow displayed around the text. A shadow will only be displayed if the <see cref="Shadow"/> property is set to true.
/// </summary>
public Color4 ShadowColour
{
get => text.ShadowColour;
set => text.ShadowColour = value;
}
/// <summary>
/// The offset of the shadow displayed around the text. A shadow will only be displayed if the <see cref="Shadow"/> property is set to true.
/// </summary>
public Vector2 ShadowOffset
{
get => text.ShadowOffset;
set => text.ShadowOffset = value;
}
/// <summary>
/// True if the <see cref="SpriteText"/>'s vertical size should be equal to <see cref="FontUsage.Size"/> (the full height) or precisely the size of used characters.
/// Set to false to allow better centering of individual characters/numerals/etc.
/// </summary>
public bool UseFullGlyphHeight
{
get => text.UseFullGlyphHeight;
set => text.UseFullGlyphHeight = value;
}
public override bool IsPresent => text.IsPresent;
public override string ToString() => text.ToString();
public float LineBaseHeight => text.LineBaseHeight;
public IEnumerable<LocalisableString> FilterTerms => text.FilterTerms;
}
}

View File

@ -81,12 +81,12 @@ namespace osu.Game.Online.API.Requests.Responses
public int? LegacyTotalScore { get; set; } public int? LegacyTotalScore { get; set; }
[JsonProperty("legacy_score_id")] [JsonProperty("legacy_score_id")]
public uint? LegacyScoreId { get; set; } public ulong? LegacyScoreId { get; set; }
#region osu-web API additions (not stored to database). #region osu-web API additions (not stored to database).
[JsonProperty("id")] [JsonProperty("id")]
public long? ID { get; set; } public ulong? ID { get; set; }
[JsonProperty("user")] [JsonProperty("user")]
public APIUser? User { get; set; } public APIUser? User { get; set; }
@ -190,6 +190,6 @@ namespace osu.Game.Online.API.Requests.Responses
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value), MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
}; };
public long OnlineID => ID ?? -1; public long OnlineID => (long?)ID ?? -1;
} }
} }

View File

@ -5,6 +5,7 @@ namespace osu.Game.Online
{ {
public enum DownloadState public enum DownloadState
{ {
Unknown,
NotDownloaded, NotDownloaded,
Downloading, Downloading,
Importing, Importing,

View File

@ -1,20 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using MessagePack;
namespace osu.Game.Online.Multiplayer.Countdown
{
/// <summary>
/// Indicates a change to the <see cref="MultiplayerRoom"/>'s countdown.
/// </summary>
[MessagePackObject]
public class CountdownChangedEvent : MatchServerEvent
{
/// <summary>
/// The new countdown.
/// </summary>
[Key(0)]
public MultiplayerCountdown? Countdown { get; set; }
}
}

View File

@ -0,0 +1,28 @@
// 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 MessagePack;
using Newtonsoft.Json;
namespace osu.Game.Online.Multiplayer.Countdown
{
/// <summary>
/// Indicates that a countdown started in the <see cref="MultiplayerRoom"/>.
/// </summary>
[MessagePackObject]
public class CountdownStartedEvent : MatchServerEvent
{
/// <summary>
/// The countdown that was started.
/// </summary>
[Key(0)]
public readonly MultiplayerCountdown Countdown;
[JsonConstructor]
[SerializationConstructor]
public CountdownStartedEvent(MultiplayerCountdown countdown)
{
Countdown = countdown;
}
}
}

View File

@ -0,0 +1,28 @@
// 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 MessagePack;
using Newtonsoft.Json;
namespace osu.Game.Online.Multiplayer.Countdown
{
/// <summary>
/// Indicates that a countdown was stopped in the <see cref="MultiplayerRoom"/>.
/// </summary>
[MessagePackObject]
public class CountdownStoppedEvent : MatchServerEvent
{
/// <summary>
/// The identifier of the countdown that was stopped.
/// </summary>
[Key(0)]
public readonly int ID;
[JsonConstructor]
[SerializationConstructor]
public CountdownStoppedEvent(int id)
{
ID = id;
}
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using MessagePack; using MessagePack;
using Newtonsoft.Json;
namespace osu.Game.Online.Multiplayer.Countdown namespace osu.Game.Online.Multiplayer.Countdown
{ {
@ -11,5 +12,14 @@ namespace osu.Game.Online.Multiplayer.Countdown
[MessagePackObject] [MessagePackObject]
public class StopCountdownRequest : MatchUserRequest public class StopCountdownRequest : MatchUserRequest
{ {
[Key(0)]
public readonly int ID;
[JsonConstructor]
[SerializationConstructor]
public StopCountdownRequest(int id)
{
ID = id;
}
} }
} }

View File

@ -13,7 +13,7 @@ namespace osu.Game.Online.Multiplayer
/// and forcing progression of any clients that are blocking load due to user interaction. /// and forcing progression of any clients that are blocking load due to user interaction.
/// </summary> /// </summary>
[MessagePackObject] [MessagePackObject]
public class ForceGameplayStartCountdown : MultiplayerCountdown public sealed class ForceGameplayStartCountdown : MultiplayerCountdown
{ {
} }
} }

View File

@ -13,7 +13,8 @@ namespace osu.Game.Online.Multiplayer
[Serializable] [Serializable]
[MessagePackObject] [MessagePackObject]
// IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
[Union(0, typeof(CountdownChangedEvent))] [Union(0, typeof(CountdownStartedEvent))]
[Union(1, typeof(CountdownStoppedEvent))]
public abstract class MatchServerEvent public abstract class MatchServerEvent
{ {
} }

View File

@ -9,7 +9,7 @@ namespace osu.Game.Online.Multiplayer
/// A <see cref="MultiplayerCountdown"/> which will start the match after ending. /// A <see cref="MultiplayerCountdown"/> which will start the match after ending.
/// </summary> /// </summary>
[MessagePackObject] [MessagePackObject]
public class MatchStartCountdown : MultiplayerCountdown public sealed class MatchStartCountdown : MultiplayerCountdown
{ {
} }
} }

View File

@ -552,8 +552,14 @@ namespace osu.Game.Online.Multiplayer
switch (e) switch (e)
{ {
case CountdownChangedEvent countdownChangedEvent: case CountdownStartedEvent countdownStartedEvent:
Room.Countdown = countdownChangedEvent.Countdown; Room.ActiveCountdowns.Add(countdownStartedEvent.Countdown);
break;
case CountdownStoppedEvent countdownStoppedEvent:
MultiplayerCountdown? countdown = Room.ActiveCountdowns.FirstOrDefault(countdown => countdown.ID == countdownStoppedEvent.ID);
if (countdown != null)
Room.ActiveCountdowns.Remove(countdown);
break; break;
} }

View File

@ -15,13 +15,24 @@ namespace osu.Game.Online.Multiplayer
[Union(1, typeof(ForceGameplayStartCountdown))] [Union(1, typeof(ForceGameplayStartCountdown))]
public abstract class MultiplayerCountdown public abstract class MultiplayerCountdown
{ {
/// <summary>
/// A unique identifier for this countdown.
/// </summary>
[Key(0)]
public int ID { get; set; }
/// <summary> /// <summary>
/// The amount of time remaining in the countdown. /// The amount of time remaining in the countdown.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This is only sent once from the server upon initial retrieval of the <see cref="MultiplayerRoom"/> or via a <see cref="CountdownChangedEvent"/>. /// This is only sent once from the server upon initial retrieval of the <see cref="MultiplayerRoom"/> or via a <see cref="CountdownStartedEvent"/>.
/// </remarks> /// </remarks>
[Key(0)] [Key(1)]
public TimeSpan TimeRemaining { get; set; } public TimeSpan TimeRemaining { get; set; }
/// <summary>
/// Whether only a single instance of this <see cref="MultiplayerCountdown"/> type may be active at any one time.
/// </summary>
public virtual bool IsExclusive => true;
} }
} }

View File

@ -53,10 +53,10 @@ namespace osu.Game.Online.Multiplayer
public IList<MultiplayerPlaylistItem> Playlist { get; set; } = new List<MultiplayerPlaylistItem>(); public IList<MultiplayerPlaylistItem> Playlist { get; set; } = new List<MultiplayerPlaylistItem>();
/// <summary> /// <summary>
/// The currently-running countdown. /// The currently running countdowns.
/// </summary> /// </summary>
[Key(7)] [Key(7)]
public MultiplayerCountdown? Countdown { get; set; } public IList<MultiplayerCountdown> ActiveCountdowns { get; set; } = new List<MultiplayerCountdown>();
[JsonConstructor] [JsonConstructor]
[SerializationConstructor] [SerializationConstructor]

View File

@ -114,6 +114,7 @@ namespace osu.Game.Online.Rooms
switch (downloadTracker.State.Value) switch (downloadTracker.State.Value)
{ {
case DownloadState.Unknown:
case DownloadState.NotDownloaded: case DownloadState.NotDownloaded:
availability.Value = BeatmapAvailability.NotDownloaded(); availability.Value = BeatmapAvailability.NotDownloaded();
break; break;

View File

@ -23,7 +23,8 @@ namespace osu.Game.Online
(typeof(ChangeTeamRequest), typeof(MatchUserRequest)), (typeof(ChangeTeamRequest), typeof(MatchUserRequest)),
(typeof(StartMatchCountdownRequest), typeof(MatchUserRequest)), (typeof(StartMatchCountdownRequest), typeof(MatchUserRequest)),
(typeof(StopCountdownRequest), typeof(MatchUserRequest)), (typeof(StopCountdownRequest), typeof(MatchUserRequest)),
(typeof(CountdownChangedEvent), typeof(MatchServerEvent)), (typeof(CountdownStartedEvent), typeof(MatchServerEvent)),
(typeof(CountdownStoppedEvent), typeof(MatchServerEvent)),
(typeof(TeamVersusRoomState), typeof(MatchRoomState)), (typeof(TeamVersusRoomState), typeof(MatchRoomState)),
(typeof(TeamVersusUserState), typeof(MatchUserState)), (typeof(TeamVersusUserState), typeof(MatchUserState)),
(typeof(MatchStartCountdown), typeof(MultiplayerCountdown)), (typeof(MatchStartCountdown), typeof(MultiplayerCountdown)),

View File

@ -839,7 +839,9 @@ namespace osu.Game
OnHome = delegate OnHome = delegate
{ {
CloseAllOverlays(false); CloseAllOverlays(false);
menuScreen?.MakeCurrent();
if (menuScreen?.GetChildScreen() != null)
menuScreen.MakeCurrent();
}, },
}, topMostOverlayContent.Add); }, topMostOverlayContent.Add);

View File

@ -390,11 +390,6 @@ namespace osu.Game
var framedClock = new FramedClock(beatmap.Track); var framedClock = new FramedClock(beatmap.Track);
beatmapClock.ChangeSource(framedClock); beatmapClock.ChangeSource(framedClock);
// Normally the internal decoupled clock will seek the *track* to the decoupled time, but we blocked this.
// It won't behave nicely unless we also set it to the track's time.
// Probably another thing which should be fixed in the decoupled mess (or just replaced).
beatmapClock.Seek(beatmap.Track.CurrentTime);
} }
protected virtual void InitialiseFonts() protected virtual void InitialiseFonts()

View File

@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Mods
/// </summary> /// </summary>
public const double FINAL_RATE_PROGRESS = 0.75f; public const double FINAL_RATE_PROGRESS = 0.75f;
public override double ScoreMultiplier => 0.5;
[SettingSource("Initial rate", "The starting speed of the track")] [SettingSource("Initial rate", "The starting speed of the track")]
public abstract BindableNumber<double> InitialRate { get; } public abstract BindableNumber<double> InitialRate { get; }

View File

@ -16,7 +16,6 @@ namespace osu.Game.Rulesets.Mods
public override string Acronym => "WD"; public override string Acronym => "WD";
public override LocalisableString Description => "Sloooow doooown..."; public override LocalisableString Description => "Sloooow doooown...";
public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleDown; public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleDown;
public override double ScoreMultiplier => 1.0;
[SettingSource("Initial rate", "The starting speed of the track")] [SettingSource("Initial rate", "The starting speed of the track")]
public override BindableNumber<double> InitialRate { get; } = new BindableDouble public override BindableNumber<double> InitialRate { get; } = new BindableDouble

View File

@ -16,7 +16,6 @@ namespace osu.Game.Rulesets.Mods
public override string Acronym => "WU"; public override string Acronym => "WU";
public override LocalisableString Description => "Can you keep up?"; public override LocalisableString Description => "Can you keep up?";
public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleUp; public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleUp;
public override double ScoreMultiplier => 1.0;
[SettingSource("Initial rate", "The starting speed of the track")] [SettingSource("Initial rate", "The starting speed of the track")]
public override BindableNumber<double> InitialRate { get; } = new BindableDouble public override BindableNumber<double> InitialRate { get; } = new BindableDouble

View File

@ -1,13 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Utils; using osu.Framework.Utils;
namespace osu.Game.Rulesets.Scoring namespace osu.Game.Rulesets.Scoring
@ -135,6 +135,8 @@ namespace osu.Game.Rulesets.Scoring
#pragma warning disable CS0618 #pragma warning disable CS0618
public static class HitResultExtensions public static class HitResultExtensions
{ {
private static readonly IList<HitResult> order = EnumExtensions.GetValuesInOrder<HitResult>().ToList();
/// <summary> /// <summary>
/// Whether a <see cref="HitResult"/> increases the combo. /// Whether a <see cref="HitResult"/> increases the combo.
/// </summary> /// </summary>
@ -282,6 +284,13 @@ namespace osu.Game.Rulesets.Scoring
Debug.Assert(minResult <= maxResult); Debug.Assert(minResult <= maxResult);
return result > minResult && result < maxResult; return result > minResult && result < maxResult;
} }
/// <summary>
/// Ordered index of a <see cref="HitResult"/>. Used for consistent order when displaying hit results to the user.
/// </summary>
/// <param name="result">The <see cref="HitResult"/> to get the index of.</param>
/// <returns>The index of <paramref name="result"/>.</returns>
public static int GetIndexForOrderedDisplay(this HitResult result) => order.IndexOf(result);
} }
#pragma warning restore CS0618 #pragma warning restore CS0618
} }

View File

@ -30,6 +30,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
@ -38,7 +39,7 @@ namespace osu.Game.Rulesets.UI
/// Displays an interactive ruleset gameplay instance. /// Displays an interactive ruleset gameplay instance.
/// </summary> /// </summary>
/// <typeparam name="TObject">The type of HitObject contained by this DrawableRuleset.</typeparam> /// <typeparam name="TObject">The type of HitObject contained by this DrawableRuleset.</typeparam>
public abstract class DrawableRuleset<TObject> : DrawableRuleset, IProvideCursor, ICanAttachKeyCounter public abstract class DrawableRuleset<TObject> : DrawableRuleset, IProvideCursor, ICanAttachHUDPieces
where TObject : HitObject where TObject : HitObject
{ {
public override event Action<JudgementResult> NewResult; public override event Action<JudgementResult> NewResult;
@ -338,7 +339,10 @@ namespace osu.Game.Rulesets.UI
public abstract DrawableHitObject<TObject> CreateDrawableRepresentation(TObject h); public abstract DrawableHitObject<TObject> CreateDrawableRepresentation(TObject h);
public void Attach(KeyCounterDisplay keyCounter) => public void Attach(KeyCounterDisplay keyCounter) =>
(KeyBindingInputManager as ICanAttachKeyCounter)?.Attach(keyCounter); (KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(keyCounter);
public void Attach(ClicksPerSecondCalculator calculator) =>
(KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(calculator);
/// <summary> /// <summary>
/// Creates a key conversion input manager. An exception will be thrown if a valid <see cref="RulesetInputManager{T}"/> is not returned. /// Creates a key conversion input manager. An exception will be thrown if a valid <see cref="RulesetInputManager{T}"/> is not returned.

View File

@ -2,15 +2,13 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -22,7 +20,7 @@ namespace osu.Game.Rulesets.UI
/// </summary> /// </summary>
[Cached(typeof(IGameplayClock))] [Cached(typeof(IGameplayClock))]
[Cached(typeof(IFrameStableClock))] [Cached(typeof(IFrameStableClock))]
public sealed class FrameStabilityContainer : Container, IHasReplayHandler, IFrameStableClock, IGameplayClock public sealed class FrameStabilityContainer : Container, IHasReplayHandler, IFrameStableClock
{ {
public ReplayInputHandler? ReplayInputHandler { get; set; } public ReplayInputHandler? ReplayInputHandler { get; set; }
@ -263,27 +261,11 @@ namespace osu.Game.Rulesets.UI
public FrameTimeInfo TimeInfo => framedClock.TimeInfo; public FrameTimeInfo TimeInfo => framedClock.TimeInfo;
public double TrueGameplayRate
{
get
{
double baseRate = Rate;
foreach (double adjustment in NonGameplayAdjustments)
{
if (Precision.AlmostEquals(adjustment, 0))
return 0;
baseRate /= adjustment;
}
return baseRate;
}
}
public double StartTime => parentGameplayClock?.StartTime ?? 0; public double StartTime => parentGameplayClock?.StartTime ?? 0;
public IEnumerable<double> NonGameplayAdjustments => parentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty<double>(); private readonly AudioAdjustments gameplayAdjustments = new AudioAdjustments();
public IAdjustableAudioComponent AdjustmentsFromMods => parentGameplayClock?.AdjustmentsFromMods ?? gameplayAdjustments;
#endregion #endregion

View File

@ -2,11 +2,11 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Timing; using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
{ {
public interface IFrameStableClock : IFrameBasedClock public interface IFrameStableClock : IGameplayClock
{ {
IBindable<bool> IsCatchingUp { get; } IBindable<bool> IsCatchingUp { get; }

View File

@ -20,11 +20,12 @@ using osu.Game.Input.Bindings;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using static osu.Game.Input.Handlers.ReplayInputHandler; using static osu.Game.Input.Handlers.ReplayInputHandler;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
{ {
public abstract class RulesetInputManager<T> : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler, IHasRecordingHandler public abstract class RulesetInputManager<T> : PassThroughInputManager, ICanAttachHUDPieces, IHasReplayHandler, IHasRecordingHandler
where T : struct where T : struct
{ {
public readonly KeyBindingContainer<T> KeyBindingContainer; public readonly KeyBindingContainer<T> KeyBindingContainer;
@ -168,7 +169,7 @@ namespace osu.Game.Rulesets.UI
.Select(action => new KeyCounterAction<T>(action))); .Select(action => new KeyCounterAction<T>(action)));
} }
public class ActionReceptor : KeyCounterDisplay.Receptor, IKeyBindingHandler<T> private class ActionReceptor : KeyCounterDisplay.Receptor, IKeyBindingHandler<T>
{ {
public ActionReceptor(KeyCounterDisplay target) public ActionReceptor(KeyCounterDisplay target)
: base(target) : base(target)
@ -186,6 +187,37 @@ namespace osu.Game.Rulesets.UI
#endregion #endregion
#region Keys per second Counter Attachment
public void Attach(ClicksPerSecondCalculator calculator)
{
var listener = new ActionListener(calculator);
KeyBindingContainer.Add(listener);
}
private class ActionListener : Component, IKeyBindingHandler<T>
{
private readonly ClicksPerSecondCalculator calculator;
public ActionListener(ClicksPerSecondCalculator calculator)
{
this.calculator = calculator;
}
public bool OnPressed(KeyBindingPressEvent<T> e)
{
calculator.AddInputTimestamp();
return false;
}
public void OnReleased(KeyBindingReleaseEvent<T> e)
{
}
}
#endregion
protected virtual KeyBindingContainer<T> CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) protected virtual KeyBindingContainer<T> CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
=> new RulesetKeyBindingContainer(ruleset, variant, unique); => new RulesetKeyBindingContainer(ruleset, variant, unique);
@ -221,12 +253,13 @@ namespace osu.Game.Rulesets.UI
} }
/// <summary> /// <summary>
/// Supports attaching a <see cref="KeyCounterDisplay"/>. /// Supports attaching various HUD pieces.
/// Keys will be populated automatically and a receptor will be injected inside. /// Keys will be populated automatically and a receptor will be injected inside.
/// </summary> /// </summary>
public interface ICanAttachKeyCounter public interface ICanAttachHUDPieces
{ {
void Attach(KeyCounterDisplay keyCounter); void Attach(KeyCounterDisplay keyCounter);
void Attach(ClicksPerSecondCalculator calculator);
} }
public class RulesetInputManagerInputState<T> : InputState public class RulesetInputManagerInputState<T> : InputState

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -16,6 +17,8 @@ using osu.Game.Scoring.Legacy;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using Realms; using Realms;
namespace osu.Game.Scoring namespace osu.Game.Scoring
@ -71,6 +74,8 @@ namespace osu.Game.Scoring
if (model.BeatmapInfo == null) throw new ArgumentNullException(nameof(model.BeatmapInfo)); if (model.BeatmapInfo == null) throw new ArgumentNullException(nameof(model.BeatmapInfo));
if (model.Ruleset == null) throw new ArgumentNullException(nameof(model.Ruleset)); if (model.Ruleset == null) throw new ArgumentNullException(nameof(model.Ruleset));
PopulateMaximumStatistics(model);
if (string.IsNullOrEmpty(model.StatisticsJson)) if (string.IsNullOrEmpty(model.StatisticsJson))
model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics); model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics);
@ -78,6 +83,68 @@ namespace osu.Game.Scoring
model.MaximumStatisticsJson = JsonConvert.SerializeObject(model.MaximumStatistics); model.MaximumStatisticsJson = JsonConvert.SerializeObject(model.MaximumStatistics);
} }
/// <summary>
/// Populates the <see cref="ScoreInfo.MaximumStatistics"/> for a given <see cref="ScoreInfo"/>.
/// </summary>
/// <param name="score">The score to populate the statistics of.</param>
public void PopulateMaximumStatistics(ScoreInfo score)
{
if (score.MaximumStatistics.Select(kvp => kvp.Value).Sum() > 0)
return;
var beatmap = score.BeatmapInfo.Detach();
var ruleset = score.Ruleset.Detach();
var rulesetInstance = ruleset.CreateInstance();
Debug.Assert(rulesetInstance != null);
// Populate the maximum statistics.
HitResult maxBasicResult = rulesetInstance.GetHitResults()
.Select(h => h.result)
.Where(h => h.IsBasic())
.OrderByDescending(Judgement.ToNumericResult).First();
foreach ((HitResult result, int count) in score.Statistics)
{
switch (result)
{
case HitResult.LargeTickHit:
case HitResult.LargeTickMiss:
score.MaximumStatistics[HitResult.LargeTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.LargeTickHit) + count;
break;
case HitResult.SmallTickHit:
case HitResult.SmallTickMiss:
score.MaximumStatistics[HitResult.SmallTickHit] = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) + count;
break;
case HitResult.IgnoreHit:
case HitResult.IgnoreMiss:
case HitResult.SmallBonus:
case HitResult.LargeBonus:
break;
default:
score.MaximumStatistics[maxBasicResult] = score.MaximumStatistics.GetValueOrDefault(maxBasicResult) + count;
break;
}
}
if (!score.IsLegacyScore)
return;
#pragma warning disable CS0618
// In osu! and osu!mania, some judgements affect combo but aren't stored to scores.
// A special hit result is used to pad out the combo value to match, based on the max combo from the difficulty attributes.
var calculator = rulesetInstance.CreateDifficultyCalculator(beatmaps().GetWorkingBeatmap(beatmap));
var attributes = calculator.Calculate(score.Mods);
int maxComboFromStatistics = score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Select(kvp => kvp.Value).DefaultIfEmpty(0).Sum();
if (attributes.MaxCombo > maxComboFromStatistics)
score.MaximumStatistics[HitResult.LegacyComboIncrease] = attributes.MaxCombo - maxComboFromStatistics;
#pragma warning restore CS0618
}
protected override void PostImport(ScoreInfo model, Realm realm, bool batchImport) protected override void PostImport(ScoreInfo model, Realm realm, bool batchImport)
{ {
base.PostImport(model, realm, batchImport); base.PostImport(model, realm, batchImport);

View File

@ -28,7 +28,8 @@ namespace osu.Game.Scoring
private readonly OsuConfigManager configManager; private readonly OsuConfigManager configManager;
private readonly ScoreImporter scoreImporter; private readonly ScoreImporter scoreImporter;
public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, RealmAccess realm, IAPIProvider api, OsuConfigManager configManager = null) public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, RealmAccess realm, IAPIProvider api,
OsuConfigManager configManager = null)
: base(storage, realm) : base(storage, realm)
{ {
this.configManager = configManager; this.configManager = configManager;
@ -178,6 +179,12 @@ namespace osu.Game.Scoring
public Live<ScoreInfo> Import(ScoreInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default) => public Live<ScoreInfo> Import(ScoreInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default) =>
scoreImporter.ImportModel(item, archive, batchImport, cancellationToken); scoreImporter.ImportModel(item, archive, batchImport, cancellationToken);
/// <summary>
/// Populates the <see cref="ScoreInfo.MaximumStatistics"/> for a given <see cref="ScoreInfo"/>.
/// </summary>
/// <param name="score">The score to populate the statistics of.</param>
public void PopulateMaximumStatistics(ScoreInfo score) => scoreImporter.PopulateMaximumStatistics(score);
#region Implementation of IPresentImports<ScoreInfo> #region Implementation of IPresentImports<ScoreInfo>
public Action<IEnumerable<Live<ScoreInfo>>> PresentImport public Action<IEnumerable<Live<ScoreInfo>>> PresentImport

View File

@ -561,6 +561,10 @@ namespace osu.Game.Screens.OnlinePlay
{ {
switch (state.NewValue) switch (state.NewValue)
{ {
case DownloadState.Unknown:
// Ignore initial state to ensure the button doesn't briefly appear.
break;
case DownloadState.LocallyAvailable: case DownloadState.LocallyAvailable:
// Perform a local query of the beatmap by beatmap checksum, and reset the state if not matching. // Perform a local query of the beatmap by beatmap checksum, and reset the state if not matching.
if (beatmapManager.QueryBeatmap(b => b.MD5Hash == beatmap.MD5Hash) == null) if (beatmapManager.QueryBeatmap(b => b.MD5Hash == beatmap.MD5Hash) == null)

View File

@ -433,6 +433,9 @@ namespace osu.Game.Screens.OnlinePlay.Match
private void updateWorkingBeatmap() private void updateWorkingBeatmap()
{ {
if (SelectedItem.Value == null || !this.IsCurrentScreen())
return;
var beatmap = SelectedItem.Value?.Beatmap; var beatmap = SelectedItem.Value?.Beatmap;
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info

View File

@ -109,7 +109,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Debug.Assert(clickOperation == null); Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation(); clickOperation = ongoingOperationTracker.BeginOperation();
if (isReady() && Client.IsHost && Room.Countdown == null) if (isReady() && Client.IsHost && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
startMatch(); startMatch();
else else
toggleReady(); toggleReady();
@ -140,10 +140,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void cancelCountdown() private void cancelCountdown()
{ {
if (Client.Room == null)
return;
Debug.Assert(clickOperation == null); Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation(); clickOperation = ongoingOperationTracker.BeginOperation();
Client.SendMatchRequest(new StopCountdownRequest()).ContinueWith(_ => endOperation()); MultiplayerCountdown countdown = Client.Room.ActiveCountdowns.Single(c => c is MatchStartCountdown);
Client.SendMatchRequest(new StopCountdownRequest(countdown.ID)).ContinueWith(_ => endOperation());
} }
private void endOperation() private void endOperation()
@ -192,7 +196,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
// When the local user is the host and spectating the match, the ready button should be enabled only if any users are ready. // When the local user is the host and spectating the match, the ready button should be enabled only if any users are ready.
if (localUser?.State == MultiplayerUserState.Spectating) if (localUser?.State == MultiplayerUserState.Spectating)
readyButton.Enabled.Value &= Client.IsHost && newCountReady > 0 && Room.Countdown == null; readyButton.Enabled.Value &= Client.IsHost && newCountReady > 0 && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown);
if (newCountReady == countReady) if (newCountReady == countReady)
return; return;

View File

@ -4,6 +4,7 @@
#nullable disable #nullable disable
using System; using System;
using System.Linq;
using Humanizer; using Humanizer;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
@ -79,7 +80,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void onRoomUpdated() => Scheduler.AddOnce(() => private void onRoomUpdated() => Scheduler.AddOnce(() =>
{ {
bool countdownActive = multiplayerClient.Room?.Countdown is MatchStartCountdown; bool countdownActive = multiplayerClient.Room?.ActiveCountdowns.Any(c => c is MatchStartCountdown) == true;
if (countdownActive) if (countdownActive)
{ {
@ -121,7 +122,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}); });
} }
if (multiplayerClient.Room?.Countdown != null && multiplayerClient.IsHost) if (multiplayerClient.Room?.ActiveCountdowns.Any(c => c is MatchStartCountdown) == true && multiplayerClient.IsHost)
{ {
flow.Add(new OsuButton flow.Add(new OsuButton
{ {

View File

@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
@ -30,12 +27,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{ {
public class MultiplayerMatchSettingsOverlay : RoomSettingsOverlay public class MultiplayerMatchSettingsOverlay : RoomSettingsOverlay
{ {
private MatchSettings settings; private MatchSettings settings = null!;
protected override OsuButton SubmitButton => settings.ApplyButton; protected override OsuButton SubmitButton => settings.ApplyButton;
[Resolved] [Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; } private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!;
protected override bool IsLoading => ongoingOperationTracker.InProgress.Value; protected override bool IsLoading => ongoingOperationTracker.InProgress.Value;
@ -57,20 +54,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{ {
private const float disabled_alpha = 0.2f; private const float disabled_alpha = 0.2f;
public Action SettingsApplied; public Action? SettingsApplied;
public OsuTextBox NameField, MaxParticipantsField; public OsuTextBox NameField = null!;
public MatchTypePicker TypePicker; public OsuTextBox MaxParticipantsField = null!;
public OsuEnumDropdown<QueueMode> QueueModeDropdown; public MatchTypePicker TypePicker = null!;
public OsuTextBox PasswordTextBox; public OsuEnumDropdown<QueueMode> QueueModeDropdown = null!;
public OsuCheckbox AutoSkipCheckbox; public OsuTextBox PasswordTextBox = null!;
public TriangleButton ApplyButton; public OsuCheckbox AutoSkipCheckbox = null!;
public TriangleButton ApplyButton = null!;
public OsuSpriteText ErrorText; public OsuSpriteText ErrorText = null!;
private OsuEnumDropdown<StartMode> startModeDropdown; private OsuEnumDropdown<StartMode> startModeDropdown = null!;
private OsuSpriteText typeLabel; private OsuSpriteText typeLabel = null!;
private LoadingLayer loadingLayer; private LoadingLayer loadingLayer = null!;
public void SelectBeatmap() public void SelectBeatmap()
{ {
@ -79,26 +77,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
} }
[Resolved] [Resolved]
private MultiplayerMatchSubScreen matchSubScreen { get; set; } private MultiplayerMatchSubScreen matchSubScreen { get; set; } = null!;
[Resolved] [Resolved]
private IRoomManager manager { get; set; } private IRoomManager manager { get; set; } = null!;
[Resolved] [Resolved]
private MultiplayerClient client { get; set; } private MultiplayerClient client { get; set; } = null!;
[Resolved] [Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; } private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!;
private readonly IBindable<bool> operationInProgress = new BindableBool(); private readonly IBindable<bool> operationInProgress = new BindableBool();
[CanBeNull]
private IDisposable applyingSettingsOperation;
private readonly Room room; private readonly Room room;
private Drawable playlistContainer; private IDisposable? applyingSettingsOperation;
private DrawableRoomPlaylist drawablePlaylist; private Drawable playlistContainer = null!;
private DrawableRoomPlaylist drawablePlaylist = null!;
public MatchSettings(Room room) public MatchSettings(Room room)
{ {
@ -423,7 +418,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
else else
room.MaxParticipants.Value = null; room.MaxParticipants.Value = null;
manager?.CreateRoom(room, onSuccess, onError); manager.CreateRoom(room, onSuccess, onError);
} }
} }
@ -466,7 +461,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
public class CreateOrUpdateButton : TriangleButton public class CreateOrUpdateButton : TriangleButton
{ {
[Resolved(typeof(Room), nameof(Room.RoomID))] [Resolved(typeof(Room), nameof(Room.RoomID))]
private Bindable<long?> roomId { get; set; } private Bindable<long?> roomId { get; set; } = null!;
protected override void LoadComplete() protected override void LoadComplete()
{ {

View File

@ -57,23 +57,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void onRoomUpdated() => Scheduler.AddOnce(() => private void onRoomUpdated() => Scheduler.AddOnce(() =>
{ {
MultiplayerCountdown newCountdown; MultiplayerCountdown newCountdown = room?.ActiveCountdowns.SingleOrDefault(c => c is MatchStartCountdown);
switch (room?.Countdown)
{
case MatchStartCountdown:
newCountdown = room.Countdown;
break;
// Clear the countdown with any other (including non-null) countdown values.
default:
newCountdown = null;
break;
}
if (newCountdown != countdown) if (newCountdown != countdown)
{ {
countdown = room?.Countdown; countdown = newCountdown;
countdownChangeTime = Time.Current; countdownChangeTime = Time.Current;
} }
@ -213,7 +201,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
case MultiplayerUserState.Spectating: case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready: case MultiplayerUserState.Ready:
if (room?.Host?.Equals(localUser) == true && room.Countdown == null) if (room?.Host?.Equals(localUser) == true && !room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
setGreen(); setGreen();
else else
setYellow(); setYellow();
@ -248,8 +236,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{ {
get get
{ {
if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready && !room.Settings.AutoStartEnabled) if (room?.ActiveCountdowns.Any(c => c is MatchStartCountdown) == true
&& multiplayerClient.IsHost
&& multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready
&& !room.Settings.AutoStartEnabled)
{
return "Cancel countdown"; return "Cancel countdown";
}
return base.TooltipText; return base.TooltipText;
} }

View File

@ -8,11 +8,9 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
@ -27,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private OngoingOperationTracker operationTracker { get; set; } = null!; private OngoingOperationTracker operationTracker { get; set; } = null!;
private readonly IBindable<bool> operationInProgress = new Bindable<bool>(); private readonly IBindable<bool> operationInProgress = new Bindable<bool>();
private readonly long? itemToEdit; private readonly PlaylistItem? itemToEdit;
private LoadingLayer loadingLayer = null!; private LoadingLayer loadingLayer = null!;
private IDisposable? selectionOperation; private IDisposable? selectionOperation;
@ -37,21 +35,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
/// </summary> /// </summary>
/// <param name="room">The room.</param> /// <param name="room">The room.</param>
/// <param name="itemToEdit">The item to be edited. May be null, in which case a new item will be added to the playlist.</param> /// <param name="itemToEdit">The item to be edited. May be null, in which case a new item will be added to the playlist.</param>
/// <param name="beatmap">An optional initial beatmap selection to perform.</param> public MultiplayerMatchSongSelect(Room room, PlaylistItem? itemToEdit = null)
/// <param name="ruleset">An optional initial ruleset selection to perform.</param> : base(room, itemToEdit)
public MultiplayerMatchSongSelect(Room room, long? itemToEdit = null, WorkingBeatmap? beatmap = null, RulesetInfo? ruleset = null)
: base(room)
{ {
this.itemToEdit = itemToEdit; this.itemToEdit = itemToEdit;
if (beatmap != null || ruleset != null)
{
Schedule(() =>
{
if (beatmap != null) Beatmap.Value = beatmap;
if (ruleset != null) Ruleset.Value = ruleset;
});
}
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -80,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{ {
if (operationInProgress.Value) if (operationInProgress.Value)
{ {
Logger.Log($"{nameof(SelectedItem)} aborted due to {nameof(operationInProgress)}"); Logger.Log($"{nameof(SelectItem)} aborted due to {nameof(operationInProgress)}");
return false; return false;
} }
@ -92,7 +79,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
var multiplayerItem = new MultiplayerPlaylistItem var multiplayerItem = new MultiplayerPlaylistItem
{ {
ID = itemToEdit ?? 0, ID = itemToEdit?.ID ?? 0,
BeatmapID = item.Beatmap.OnlineID, BeatmapID = item.Beatmap.OnlineID,
BeatmapChecksum = item.Beatmap.MD5Hash, BeatmapChecksum = item.Beatmap.MD5Hash,
RulesetID = item.RulesetID, RulesetID = item.RulesetID,

View File

@ -49,11 +49,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[Resolved] [Resolved]
private MultiplayerClient client { get; set; } private MultiplayerClient client { get; set; }
[Resolved]
private BeatmapManager beatmapManager { get; set; }
private readonly IBindable<bool> isConnected = new Bindable<bool>();
private AddItemButton addItemButton; private AddItemButton addItemButton;
public MultiplayerMatchSubScreen(Room room) public MultiplayerMatchSubScreen(Room room)
@ -227,12 +222,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (!this.IsCurrentScreen()) if (!this.IsCurrentScreen())
return; return;
int id = itemToEdit?.Beatmap.OnlineID ?? Room.Playlist.Last().Beatmap.OnlineID; this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit));
var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == id);
var workingBeatmap = localBeatmap == null ? null : beatmapManager.GetWorkingBeatmap(localBeatmap);
this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit?.ID, workingBeatmap));
} }
protected override Drawable CreateFooter() => new MultiplayerMatchFooter(); protected override Drawable CreateFooter() => new MultiplayerMatchFooter();
@ -424,7 +414,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return; return;
} }
this.Push(new MultiplayerMatchSongSelect(Room, client.Room.Settings.PlaylistItemId, beatmap, ruleset)); this.Push(new MultiplayerMatchSongSelect(Room, Room.Playlist.Single(item => item.ID == client.Room.Settings.PlaylistItemId)));
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -13,6 +14,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// </summary> /// </summary>
public class MultiSpectatorPlayer : SpectatorPlayer public class MultiSpectatorPlayer : SpectatorPlayer
{ {
/// <summary>
/// All adjustments applied to the clock of this <see cref="MultiSpectatorPlayer"/> which come from mods.
/// </summary>
public IAggregateAudioAdjustment ClockAdjustmentsFromMods => clockAdjustmentsFromMods;
private readonly AudioAdjustments clockAdjustmentsFromMods = new AudioAdjustments();
private readonly SpectatorPlayerClock spectatorPlayerClock; private readonly SpectatorPlayerClock spectatorPlayerClock;
/// <summary> /// <summary>
@ -53,6 +60,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
} }
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
=> new GameplayClockContainer(spectatorPlayerClock); {
var gameplayClockContainer = new GameplayClockContainer(spectatorPlayerClock);
clockAdjustmentsFromMods.BindAdjustments(gameplayClockContainer.AdjustmentsFromMods);
return gameplayClockContainer;
}
} }
} }

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -43,6 +44,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
[Resolved] [Resolved]
private MultiplayerClient multiplayerClient { get; set; } = null!; private MultiplayerClient multiplayerClient { get; set; } = null!;
private IAggregateAudioAdjustment? boundAdjustments;
private readonly PlayerArea[] instances; private readonly PlayerArea[] instances;
private MasterGameplayClockContainer masterClockContainer = null!; private MasterGameplayClockContainer masterClockContainer = null!;
private SpectatorSyncManager syncManager = null!; private SpectatorSyncManager syncManager = null!;
@ -157,6 +160,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
base.LoadComplete(); base.LoadComplete();
masterClockContainer.Reset(); masterClockContainer.Reset();
// Start with adjustments from the first player to keep a sane state.
bindAudioAdjustments(instances.First());
} }
protected override void Update() protected override void Update()
@ -169,11 +175,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
.OrderBy(i => Math.Abs(i.SpectatorPlayerClock.CurrentTime - syncManager.CurrentMasterTime)) .OrderBy(i => Math.Abs(i.SpectatorPlayerClock.CurrentTime - syncManager.CurrentMasterTime))
.FirstOrDefault(); .FirstOrDefault();
// Only bind adjustments if there's actually a valid source, else just use the previous ones to ensure no sudden changes to audio.
if (currentAudioSource != null)
bindAudioAdjustments(currentAudioSource);
foreach (var instance in instances) foreach (var instance in instances)
instance.Mute = instance != currentAudioSource; instance.Mute = instance != currentAudioSource;
} }
} }
private void bindAudioAdjustments(PlayerArea first)
{
if (boundAdjustments != null)
masterClockContainer.AdjustmentsFromMods.UnbindAdjustments(boundAdjustments);
boundAdjustments = first.ClockAdjustmentsFromMods;
masterClockContainer.AdjustmentsFromMods.BindAdjustments(boundAdjustments);
}
private bool isCandidateAudioSource(SpectatorPlayerClock? clock) private bool isCandidateAudioSource(SpectatorPlayerClock? clock)
=> clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames; => clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames;

View File

@ -42,6 +42,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// </summary> /// </summary>
public readonly SpectatorPlayerClock SpectatorPlayerClock; public readonly SpectatorPlayerClock SpectatorPlayerClock;
/// <summary>
/// The clock adjustments applied by the <see cref="Player"/> loaded in this area.
/// </summary>
public IAggregateAudioAdjustment ClockAdjustmentsFromMods => clockAdjustmentsFromMods;
/// <summary> /// <summary>
/// The currently-loaded score. /// The currently-loaded score.
/// </summary> /// </summary>
@ -50,6 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
[Resolved] [Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!; private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
private readonly AudioAdjustments clockAdjustmentsFromMods = new AudioAdjustments();
private readonly BindableDouble volumeAdjustment = new BindableDouble(); private readonly BindableDouble volumeAdjustment = new BindableDouble();
private readonly Container gameplayContent; private readonly Container gameplayContent;
private readonly LoadingLayer loadingLayer; private readonly LoadingLayer loadingLayer;
@ -97,6 +103,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{ {
var player = new MultiSpectatorPlayer(Score, SpectatorPlayerClock); var player = new MultiSpectatorPlayer(Score, SpectatorPlayerClock);
player.OnGameplayStarted += () => OnGameplayStarted?.Invoke(); player.OnGameplayStarted += () => OnGameplayStarted?.Invoke();
clockAdjustmentsFromMods.BindAdjustments(player.ClockAdjustmentsFromMods);
return player; return player;
})); }));

View File

@ -1,13 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using Humanizer; using Humanizer;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -35,32 +33,34 @@ namespace osu.Game.Screens.OnlinePlay
public override bool AllowEditing => false; public override bool AllowEditing => false;
[Resolved(typeof(Room), nameof(Room.Playlist))] [Resolved(typeof(Room), nameof(Room.Playlist))]
protected BindableList<PlaylistItem> Playlist { get; private set; } protected BindableList<PlaylistItem> Playlist { get; private set; } = null!;
[CanBeNull]
[Resolved(CanBeNull = true)]
protected IBindable<PlaylistItem> SelectedItem { get; private set; }
[Resolved] [Resolved]
private RulesetStore rulesets { get; set; } private RulesetStore rulesets { get; set; } = null!;
[Resolved]
private BeatmapManager beatmapManager { get; set; } = null!;
protected override UserActivity InitialActivity => new UserActivity.InLobby(room); protected override UserActivity InitialActivity => new UserActivity.InLobby(room);
protected readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>()); protected readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
private readonly Room room; private readonly Room room;
private readonly PlaylistItem? initialItem;
private WorkingBeatmap initialBeatmap;
private RulesetInfo initialRuleset;
private IReadOnlyList<Mod> initialMods;
private bool itemSelected;
private readonly FreeModSelectOverlay freeModSelectOverlay; private readonly FreeModSelectOverlay freeModSelectOverlay;
private IDisposable freeModSelectOverlayRegistration;
protected OnlinePlaySongSelect(Room room) private IDisposable? freeModSelectOverlayRegistration;
/// <summary>
/// Creates a new <see cref="OnlinePlaySongSelect"/>.
/// </summary>
/// <param name="room">The room.</param>
/// <param name="initialItem">An optional initial <see cref="PlaylistItem"/> to use for the initial beatmap/ruleset/mods.
/// If <c>null</c>, the last <see cref="PlaylistItem"/> in the room will be used.</param>
protected OnlinePlaySongSelect(Room room, PlaylistItem? initialItem = null)
{ {
this.room = room; this.room = room;
this.initialItem = initialItem ?? room.Playlist.LastOrDefault();
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
@ -75,11 +75,6 @@ namespace osu.Game.Screens.OnlinePlay
private void load() private void load()
{ {
LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT }; LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT };
initialBeatmap = Beatmap.Value;
initialRuleset = Ruleset.Value;
initialMods = Mods.Value.ToList();
LoadComponent(freeModSelectOverlay); LoadComponent(freeModSelectOverlay);
} }
@ -87,14 +82,35 @@ namespace osu.Game.Screens.OnlinePlay
{ {
base.LoadComplete(); base.LoadComplete();
var rulesetInstance = SelectedItem?.Value?.RulesetID == null ? null : rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); if (initialItem != null)
if (rulesetInstance != null)
{ {
// At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods. // Prefer using a local databased beatmap lookup since OnlineId may be -1 for an invalid beatmap selection.
// Similarly, freeMods is currently empty but should only contain the allowed mods. BeatmapInfo? beatmapInfo = initialItem.Beatmap as BeatmapInfo;
Mods.Value = SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
FreeMods.Value = SelectedItem.Value.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); // And in the case that this isn't a local databased beatmap, query by online ID.
if (beatmapInfo == null)
{
int onlineId = initialItem.Beatmap.OnlineID;
beatmapInfo = beatmapManager.QueryBeatmap(b => b.OnlineID == onlineId);
}
if (beatmapInfo != null)
Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo);
RulesetInfo? ruleset = rulesets.GetRuleset(initialItem.RulesetID);
if (ruleset != null)
{
Ruleset.Value = ruleset;
var rulesetInstance = ruleset.CreateInstance();
Debug.Assert(rulesetInstance != null);
// At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods.
// Similarly, freeMods is currently empty but should only contain the allowed mods.
Mods.Value = initialItem.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
FreeMods.Value = initialItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
}
} }
Mods.BindValueChanged(onModsChanged); Mods.BindValueChanged(onModsChanged);
@ -125,13 +141,7 @@ namespace osu.Game.Screens.OnlinePlay
AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray() AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray()
}; };
if (SelectItem(item)) return SelectItem(item);
{
itemSelected = true;
return true;
}
return false;
} }
/// <summary> /// <summary>
@ -154,15 +164,7 @@ namespace osu.Game.Screens.OnlinePlay
public override bool OnExiting(ScreenExitEvent e) public override bool OnExiting(ScreenExitEvent e)
{ {
if (!itemSelected)
{
Beatmap.Value = initialBeatmap;
Ruleset.Value = initialRuleset;
Mods.Value = initialMods;
}
freeModSelectOverlay.Hide(); freeModSelectOverlay.Hide();
return base.OnExiting(e); return base.OnExiting(e);
} }
@ -199,7 +201,6 @@ namespace osu.Game.Screens.OnlinePlay
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
freeModSelectOverlayRegistration?.Dispose(); freeModSelectOverlayRegistration?.Dispose();
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq; using System.Linq;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Online.API; using osu.Game.Online.API;

View File

@ -2,15 +2,13 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
@ -46,7 +44,7 @@ namespace osu.Game.Screens.Play
/// </remarks> /// </remarks>
public double StartTime { get; protected set; } public double StartTime { get; protected set; }
public virtual IEnumerable<double> NonGameplayAdjustments => Enumerable.Empty<double>(); public IAdjustableAudioComponent AdjustmentsFromMods { get; } = new AudioAdjustments();
private readonly BindableBool isPaused = new BindableBool(true); private readonly BindableBool isPaused = new BindableBool(true);
@ -196,7 +194,9 @@ namespace osu.Game.Screens.Play
void IAdjustableClock.Reset() => Reset(); void IAdjustableClock.Reset() => Reset();
public void ResetSpeedAdjustments() => throw new NotImplementedException(); public virtual void ResetSpeedAdjustments()
{
}
double IAdjustableClock.Rate double IAdjustableClock.Rate
{ {
@ -222,23 +222,5 @@ namespace osu.Game.Screens.Play
public double FramesPerSecond => GameplayClock.FramesPerSecond; public double FramesPerSecond => GameplayClock.FramesPerSecond;
public FrameTimeInfo TimeInfo => GameplayClock.TimeInfo; public FrameTimeInfo TimeInfo => GameplayClock.TimeInfo;
public double TrueGameplayRate
{
get
{
double baseRate = Rate;
foreach (double adjustment in NonGameplayAdjustments)
{
if (Precision.AlmostEquals(adjustment, 0))
return 0;
baseRate /= adjustment;
}
return baseRate;
}
}
} }
} }

View File

@ -0,0 +1,24 @@
// 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;
namespace osu.Game.Screens.Play
{
public static class GameplayClockExtensions
{
/// <summary>
/// The rate of gameplay when playback is at 100%.
/// This excludes any seeking / user adjustments.
/// </summary>
public static double GetTrueGameplayRate(this IGameplayClock clock)
{
// To handle rewind, we still want to maintain the same direction as the underlying clock.
double rate = clock.Rate == 0 ? 1 : Math.Sign(clock.Rate);
return rate
* clock.AdjustmentsFromMods.AggregateFrequency.Value
* clock.AdjustmentsFromMods.AggregateTempo.Value;
}
}
}

View File

@ -0,0 +1,59 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.UI;
namespace osu.Game.Screens.Play.HUD.ClicksPerSecond
{
public class ClicksPerSecondCalculator : Component
{
private readonly List<double> timestamps = new List<double>();
[Resolved]
private IGameplayClock gameplayClock { get; set; } = null!;
[Resolved(canBeNull: true)]
private DrawableRuleset? drawableRuleset { get; set; }
public int Value { get; private set; }
// Even though `FrameStabilityContainer` caches as a `GameplayClock`, we need to check it directly via `drawableRuleset`
// as this calculator is not contained within the `FrameStabilityContainer` and won't see the dependency.
private IGameplayClock clock => drawableRuleset?.FrameStableClock ?? gameplayClock;
public ClicksPerSecondCalculator()
{
RelativeSizeAxes = Axes.Both;
}
public void AddInputTimestamp() => timestamps.Add(clock.CurrentTime);
protected override void Update()
{
base.Update();
double latestValidTime = clock.CurrentTime;
double earliestTimeValid = latestValidTime - 1000 * gameplayClock.GetTrueGameplayRate();
int count = 0;
for (int i = timestamps.Count - 1; i >= 0; i--)
{
// handle rewinding by removing future timestamps as we go
if (timestamps[i] > latestValidTime)
{
timestamps.RemoveAt(i);
continue;
}
if (timestamps[i] >= earliestTimeValid)
count++;
}
Value = count;
}
}
}

View File

@ -0,0 +1,102 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Screens.Play.HUD.ClicksPerSecond
{
public class ClicksPerSecondCounter : RollingCounter<int>, ISkinnableDrawable
{
[Resolved]
private ClicksPerSecondCalculator calculator { get; set; } = null!;
protected override double RollingDuration => 350;
public bool UsesFixedAnchor { get; set; }
public ClicksPerSecondCounter()
{
Current.Value = 0;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = colours.BlueLighter;
}
protected override void Update()
{
base.Update();
Current.Value = calculator.Value;
}
protected override IHasText CreateText() => new TextComponent();
private class TextComponent : CompositeDrawable, IHasText
{
public LocalisableString Text
{
get => text.Text;
set => text.Text = value;
}
private readonly OsuSpriteText text;
public TextComponent()
{
AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(2),
Children = new Drawable[]
{
text = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.Numeric.With(size: 16, fixedWidth: true)
},
new FillFlowContainer
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Font = OsuFont.Numeric.With(size: 6, fixedWidth: false),
Text = @"clicks",
},
new OsuSpriteText
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Font = OsuFont.Numeric.With(size: 6, fixedWidth: false),
Text = @"/sec",
Padding = new MarginPadding { Bottom = 3f }, // align baseline better
}
}
}
}
};
}
}
}
}

View File

@ -16,7 +16,6 @@ namespace osu.Game.Screens.Play.HUD
{ {
public class DefaultSongProgress : SongProgress public class DefaultSongProgress : SongProgress
{ {
private const float info_height = 20;
private const float bottom_bar_height = 5; private const float bottom_bar_height = 5;
private const float graph_height = SquareGraph.Column.WIDTH * 6; private const float graph_height = SquareGraph.Column.WIDTH * 6;
private const float handle_height = 18; private const float handle_height = 18;
@ -65,7 +64,6 @@ namespace osu.Game.Screens.Play.HUD
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = info_height,
}, },
graph = new SongProgressGraph graph = new SongProgressGraph
{ {
@ -178,7 +176,7 @@ namespace osu.Game.Screens.Play.HUD
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
Height = bottom_bar_height + graph_height + handle_size.Y + info_height - graph.Y; Height = bottom_bar_height + graph_height + handle_size.Y + info.Height - graph.Y;
} }
private void updateBarVisibility() private void updateBarVisibility()

View File

@ -59,30 +59,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
protected Color4 GetColourForHitResult(HitResult result) protected Color4 GetColourForHitResult(HitResult result)
{ {
switch (result) return colours.ForHitResult(result);
{
case HitResult.SmallTickMiss:
case HitResult.LargeTickMiss:
case HitResult.Miss:
return colours.Red;
case HitResult.Meh:
return colours.Yellow;
case HitResult.Ok:
return colours.Green;
case HitResult.Good:
return colours.GreenLight;
case HitResult.SmallTickHit:
case HitResult.LargeTickHit:
case HitResult.Great:
return colours.Blue;
default:
return colours.BlueLight;
}
} }
/// <summary> /// <summary>

View File

@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using System; using System;
@ -14,9 +15,9 @@ namespace osu.Game.Screens.Play.HUD
{ {
public class SongProgressInfo : Container public class SongProgressInfo : Container
{ {
private OsuSpriteText timeCurrent; private SizePreservingSpriteText timeCurrent;
private OsuSpriteText timeLeft; private SizePreservingSpriteText timeLeft;
private OsuSpriteText progress; private SizePreservingSpriteText progress;
private double startTime; private double startTime;
private double endTime; private double endTime;
@ -46,36 +47,71 @@ namespace osu.Game.Screens.Play.HUD
if (clock != null) if (clock != null)
gameplayClock = clock; gameplayClock = clock;
AutoSizeAxes = Axes.Y;
Children = new Drawable[] Children = new Drawable[]
{ {
timeCurrent = new OsuSpriteText new Container
{ {
Origin = Anchor.BottomLeft, Anchor = Anchor.CentreLeft,
Anchor = Anchor.BottomLeft, Origin = Anchor.CentreLeft,
Colour = colours.BlueLighter, AutoSizeAxes = Axes.Both,
Font = OsuFont.Numeric, Child = new UprightAspectMaintainingContainer
Margin = new MarginPadding
{ {
Left = margin, Origin = Anchor.Centre,
}, Anchor = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Scaling = ScaleMode.Vertical,
ScalingFactor = 0.5f,
Child = timeCurrent = new SizePreservingSpriteText
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Colour = colours.BlueLighter,
Font = OsuFont.Numeric,
}
}
}, },
progress = new OsuSpriteText new Container
{ {
Origin = Anchor.BottomCentre, Origin = Anchor.Centre,
Anchor = Anchor.BottomCentre, Anchor = Anchor.Centre,
Colour = colours.BlueLighter, AutoSizeAxes = Axes.Both,
Font = OsuFont.Numeric, Child = new UprightAspectMaintainingContainer
},
timeLeft = new OsuSpriteText
{
Origin = Anchor.BottomRight,
Anchor = Anchor.BottomRight,
Colour = colours.BlueLighter,
Font = OsuFont.Numeric,
Margin = new MarginPadding
{ {
Right = margin, Origin = Anchor.Centre,
}, Anchor = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Scaling = ScaleMode.Vertical,
ScalingFactor = 0.5f,
Child = progress = new SizePreservingSpriteText
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Colour = colours.BlueLighter,
Font = OsuFont.Numeric,
}
}
},
new Container
{
Origin = Anchor.CentreRight,
Anchor = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
Child = new UprightAspectMaintainingContainer
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Scaling = ScaleMode.Vertical,
ScalingFactor = 0.5f,
Child = timeLeft = new SizePreservingSpriteText
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Colour = colours.BlueLighter,
Font = OsuFont.Numeric,
}
}
} }
}; };
} }

View File

@ -22,6 +22,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
@ -49,6 +50,9 @@ namespace osu.Game.Screens.Play
public readonly HoldForMenuButton HoldToQuit; public readonly HoldForMenuButton HoldToQuit;
public readonly PlayerSettingsOverlay PlayerSettingsOverlay; public readonly PlayerSettingsOverlay PlayerSettingsOverlay;
[Cached]
private readonly ClicksPerSecondCalculator clicksPerSecondCalculator;
public Bindable<bool> ShowHealthBar = new Bindable<bool>(true); public Bindable<bool> ShowHealthBar = new Bindable<bool>(true);
private readonly DrawableRuleset drawableRuleset; private readonly DrawableRuleset drawableRuleset;
@ -122,7 +126,8 @@ namespace osu.Game.Screens.Play
KeyCounter = CreateKeyCounter(), KeyCounter = CreateKeyCounter(),
HoldToQuit = CreateHoldForMenuButton(), HoldToQuit = CreateHoldForMenuButton(),
} }
} },
clicksPerSecondCalculator = new ClicksPerSecondCalculator()
}; };
} }
@ -259,7 +264,11 @@ namespace osu.Game.Screens.Play
protected virtual void BindDrawableRuleset(DrawableRuleset drawableRuleset) protected virtual void BindDrawableRuleset(DrawableRuleset drawableRuleset)
{ {
(drawableRuleset as ICanAttachKeyCounter)?.Attach(KeyCounter); if (drawableRuleset is ICanAttachHUDPieces attachTarget)
{
attachTarget.Attach(KeyCounter);
attachTarget.Attach(clicksPerSecondCalculator);
}
replayLoaded.BindTo(drawableRuleset.HasReplayLoaded); replayLoaded.BindTo(drawableRuleset.HasReplayLoaded);
} }

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Timing; using osu.Framework.Timing;
@ -9,12 +9,6 @@ namespace osu.Game.Screens.Play
{ {
public interface IGameplayClock : IFrameBasedClock public interface IGameplayClock : IFrameBasedClock
{ {
/// <summary>
/// The rate of gameplay when playback is at 100%.
/// This excludes any seeking / user adjustments.
/// </summary>
double TrueGameplayRate { get; }
/// <summary> /// <summary>
/// The time from which the clock should start. Will be seeked to on calling <see cref="GameplayClockContainer.Reset"/>. /// The time from which the clock should start. Will be seeked to on calling <see cref="GameplayClockContainer.Reset"/>.
/// </summary> /// </summary>
@ -25,9 +19,9 @@ namespace osu.Game.Screens.Play
double StartTime { get; } double StartTime { get; }
/// <summary> /// <summary>
/// All adjustments applied to this clock which don't come from gameplay or mods. /// All adjustments applied to this clock which come from mods.
/// </summary> /// </summary>
IEnumerable<double> NonGameplayAdjustments { get; } IAdjustableAudioComponent AdjustmentsFromMods { get; }
IBindable<bool> IsPaused { get; } IBindable<bool> IsPaused { get; }
} }

View File

@ -2,8 +2,8 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -11,6 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Overlays;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
@ -41,9 +42,9 @@ namespace osu.Game.Screens.Play
private readonly WorkingBeatmap beatmap; private readonly WorkingBeatmap beatmap;
private readonly double skipTargetTime; private readonly Track track;
private readonly List<Bindable<double>> nonGameplayAdjustments = new List<Bindable<double>>(); private readonly double skipTargetTime;
/// <summary> /// <summary>
/// Stores the time at which the last <see cref="StopGameplayClock"/> call was triggered. /// Stores the time at which the last <see cref="StopGameplayClock"/> call was triggered.
@ -56,7 +57,8 @@ namespace osu.Game.Screens.Play
/// </summary> /// </summary>
private double? actualStopTime; private double? actualStopTime;
public override IEnumerable<double> NonGameplayAdjustments => nonGameplayAdjustments.Select(b => b.Value); [Resolved]
private MusicController musicController { get; set; } = null!;
/// <summary> /// <summary>
/// Create a new master gameplay clock container. /// Create a new master gameplay clock container.
@ -69,6 +71,8 @@ namespace osu.Game.Screens.Play
this.beatmap = beatmap; this.beatmap = beatmap;
this.skipTargetTime = skipTargetTime; this.skipTargetTime = skipTargetTime;
track = beatmap.Track;
StartTime = findEarliestStartTime(); StartTime = findEarliestStartTime();
} }
@ -195,15 +199,12 @@ namespace osu.Game.Screens.Play
if (speedAdjustmentsApplied) if (speedAdjustmentsApplied)
return; return;
if (SourceClock is not Track track) musicController.ResetTrackAdjustments();
return;
track.BindAdjustments(AdjustmentsFromMods);
track.AddAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust); track.AddAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust);
track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
nonGameplayAdjustments.Add(GameplayClock.ExternalPauseFrequencyAdjust);
nonGameplayAdjustments.Add(UserPlaybackRate);
speedAdjustmentsApplied = true; speedAdjustmentsApplied = true;
} }
@ -212,15 +213,10 @@ namespace osu.Game.Screens.Play
if (!speedAdjustmentsApplied) if (!speedAdjustmentsApplied)
return; return;
if (SourceClock is not Track track) track.UnbindAdjustments(AdjustmentsFromMods);
return;
track.RemoveAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust); track.RemoveAdjustment(AdjustableProperty.Frequency, GameplayClock.ExternalPauseFrequencyAdjust);
track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
nonGameplayAdjustments.Remove(GameplayClock.ExternalPauseFrequencyAdjust);
nonGameplayAdjustments.Remove(UserPlaybackRate);
speedAdjustmentsApplied = false; speedAdjustmentsApplied = false;
} }

View File

@ -996,12 +996,8 @@ namespace osu.Game.Screens.Play
foreach (var mod in GameplayState.Mods.OfType<IApplicableToHUD>()) foreach (var mod in GameplayState.Mods.OfType<IApplicableToHUD>())
mod.ApplyToHUD(HUDOverlay); mod.ApplyToHUD(HUDOverlay);
// Our mods are local copies of the global mods so they need to be re-applied to the track.
// This is done through the music controller (for now), because resetting speed adjustments on the beatmap track also removes adjustments provided by DrawableTrack.
// Todo: In the future, player will receive in a track and will probably not have to worry about this...
musicController.ResetTrackAdjustments();
foreach (var mod in GameplayState.Mods.OfType<IApplicableToTrack>()) foreach (var mod in GameplayState.Mods.OfType<IApplicableToTrack>())
mod.ApplyToTrack(musicController.CurrentTrack); mod.ApplyToTrack(GameplayClockContainer.AdjustmentsFromMods);
updateGameplayState(); updateGameplayState();
@ -1053,6 +1049,7 @@ namespace osu.Game.Screens.Play
musicController.ResetTrackAdjustments(); musicController.ResetTrackAdjustments();
fadeOut(); fadeOut();
return base.OnExiting(e); return base.OnExiting(e);
} }

View File

@ -63,8 +63,7 @@ namespace osu.Game.Screens.Play
if (player != null) if (player != null)
{ {
importedScore = realm.Run(r => r.Find<ScoreInfo>(player.Score.ScoreInfo.ID)?.Detach()); importedScore = realm.Run(r => r.Find<ScoreInfo>(player.Score.ScoreInfo.ID)?.Detach());
if (importedScore != null) state.Value = importedScore != null ? DownloadState.LocallyAvailable : DownloadState.NotDownloaded;
state.Value = DownloadState.LocallyAvailable;
} }
state.BindValueChanged(state => state.BindValueChanged(state =>

View File

@ -7,7 +7,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -57,7 +56,7 @@ namespace osu.Game.Screens.Ranking.Statistics
this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit()).ToList(); this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit()).ToList();
} }
private int[] bins; private IDictionary<HitResult, int>[] bins;
private double binSize; private double binSize;
private double hitOffset; private double hitOffset;
@ -69,7 +68,7 @@ namespace osu.Game.Screens.Ranking.Statistics
if (hitEvents == null || hitEvents.Count == 0) if (hitEvents == null || hitEvents.Count == 0)
return; return;
bins = new int[total_timing_distribution_bins]; bins = Enumerable.Range(0, total_timing_distribution_bins).Select(_ => new Dictionary<HitResult, int>()).ToArray<IDictionary<HitResult, int>>();
binSize = Math.Ceiling(hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins); binSize = Math.Ceiling(hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins);
@ -89,7 +88,8 @@ namespace osu.Game.Screens.Ranking.Statistics
{ {
bool roundUp = true; bool roundUp = true;
Array.Clear(bins, 0, bins.Length); foreach (var bin in bins)
bin.Clear();
foreach (var e in hitEvents) foreach (var e in hitEvents)
{ {
@ -110,23 +110,23 @@ namespace osu.Game.Screens.Ranking.Statistics
// may be out of range when applying an offset. for such cases we can just drop the results. // may be out of range when applying an offset. for such cases we can just drop the results.
if (index >= 0 && index < bins.Length) if (index >= 0 && index < bins.Length)
bins[index]++; {
bins[index].TryGetValue(e.Result, out int value);
bins[index][e.Result] = ++value;
}
} }
if (barDrawables != null) if (barDrawables != null)
{ {
for (int i = 0; i < barDrawables.Length; i++) for (int i = 0; i < barDrawables.Length; i++)
{ {
barDrawables[i].UpdateOffset(bins[i]); barDrawables[i].UpdateOffset(bins[i].Sum(b => b.Value));
} }
} }
else else
{ {
int maxCount = bins.Max(); int maxCount = bins.Max(b => b.Values.Sum());
barDrawables = new Bar[total_timing_distribution_bins]; barDrawables = bins.Select((bin, i) => new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index)).ToArray();
for (int i = 0; i < barDrawables.Length; i++)
barDrawables[i] = new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index);
Container axisFlow; Container axisFlow;
@ -209,50 +209,97 @@ namespace osu.Game.Screens.Ranking.Statistics
private class Bar : CompositeDrawable private class Bar : CompositeDrawable
{ {
private readonly float value; private float totalValue => values.Sum(v => v.Value);
private readonly float maxValue; private float basalHeight => BoundingBox.Width / BoundingBox.Height;
private float availableHeight => 1 - basalHeight;
private readonly Circle boxOriginal; private readonly IReadOnlyList<KeyValuePair<HitResult, int>> values;
private readonly float maxValue;
private readonly bool isCentre;
private Circle[] boxOriginals;
private Circle boxAdjustment; private Circle boxAdjustment;
private const float minimum_height = 0.05f; [Resolved]
private OsuColour colours { get; set; }
public Bar(float value, float maxValue, bool isCentre) public Bar(IDictionary<HitResult, int> values, float maxValue, bool isCentre)
{ {
this.value = value; this.values = values.OrderBy(v => v.Key.GetIndexForOrderedDisplay()).ToList();
this.maxValue = maxValue; this.maxValue = maxValue;
this.isCentre = isCentre;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Masking = true; Masking = true;
}
InternalChildren = new Drawable[] [BackgroundDependencyLoader]
private void load()
{
if (values.Any())
{ {
boxOriginal = new Circle boxOriginals = values.Select((v, i) => new Circle
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
Colour = isCentre ? Color4.White : Color4Extensions.FromHex("#66FFCC"), Colour = isCentre && i == 0 ? Color4.White : colours.ForHitResult(v.Key),
Height = minimum_height, Height = 0,
}, }).ToArray();
}; // The bars of the stacked bar graph will be processed (stacked) from the bottom, which is the base position,
// to the top, and the bottom bar should be drawn more toward the front by design,
// while the drawing order is from the back to the front, so the order passed to `InternalChildren` is the opposite.
InternalChildren = boxOriginals.Reverse().ToArray();
}
else
{
// A bin with no value draws a grey dot instead.
InternalChildren = boxOriginals = new[]
{
new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Colour = isCentre ? Color4.White : Color4.Gray,
Height = 0,
},
};
}
} }
private const double duration = 300; private const double duration = 300;
private float offsetForValue(float value)
{
return availableHeight * value / maxValue;
}
private float heightForValue(float value)
{
return basalHeight + offsetForValue(value);
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
float height = Math.Clamp(value / maxValue, minimum_height, 1); foreach (var boxOriginal in boxOriginals)
boxOriginal.Height = basalHeight;
if (height > minimum_height) float offsetValue = 0;
boxOriginal.ResizeHeightTo(height, duration, Easing.OutQuint);
for (int i = 0; i < values.Count; i++)
{
boxOriginals[i].MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint);
boxOriginals[i].ResizeHeightTo(heightForValue(values[i].Value), duration, Easing.OutQuint);
offsetValue -= values[i].Value;
}
} }
public void UpdateOffset(float adjustment) public void UpdateOffset(float adjustment)
{ {
bool hasAdjustment = adjustment != value && adjustment / maxValue >= minimum_height; bool hasAdjustment = adjustment != totalValue;
if (boxAdjustment == null) if (boxAdjustment == null)
{ {
@ -271,7 +318,7 @@ namespace osu.Game.Screens.Ranking.Statistics
}); });
} }
boxAdjustment.ResizeHeightTo(Math.Clamp(adjustment / maxValue, minimum_height, 1), duration, Easing.OutQuint); boxAdjustment.ResizeHeightTo(heightForValue(adjustment), duration, Easing.OutQuint);
boxAdjustment.FadeTo(!hasAdjustment ? 0 : 1, duration, Easing.OutQuint); boxAdjustment.FadeTo(!hasAdjustment ? 0 : 1, duration, Easing.OutQuint);
} }
} }

View File

@ -65,6 +65,13 @@ namespace osu.Game.Screens.Select
private class MinimumStarsSlider : StarsSlider private class MinimumStarsSlider : StarsSlider
{ {
public MinimumStarsSlider()
: base("0")
{
}
public override LocalisableString TooltipText => Current.Value.ToString(@"0.## stars");
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -82,6 +89,11 @@ namespace osu.Game.Screens.Select
private class MaximumStarsSlider : StarsSlider private class MaximumStarsSlider : StarsSlider
{ {
public MaximumStarsSlider()
: base("∞")
{
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -96,10 +108,17 @@ namespace osu.Game.Screens.Select
private class StarsSlider : OsuSliderBar<double> private class StarsSlider : OsuSliderBar<double>
{ {
private readonly string defaultString;
public override LocalisableString TooltipText => Current.IsDefault public override LocalisableString TooltipText => Current.IsDefault
? UserInterfaceStrings.NoLimit ? UserInterfaceStrings.NoLimit
: Current.Value.ToString(@"0.## stars"); : Current.Value.ToString(@"0.## stars");
protected StarsSlider(string defaultString)
{
this.defaultString = defaultString;
}
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
base.OnHover(e); base.OnHover(e);
@ -125,7 +144,7 @@ namespace osu.Game.Screens.Select
Current.BindValueChanged(current => Current.BindValueChanged(current =>
{ {
currentDisplay.Text = current.NewValue != Current.Default ? current.NewValue.ToString("N1") : "∞"; currentDisplay.Text = current.NewValue != Current.Default ? current.NewValue.ToString("N1") : defaultString;
}, true); }, true);
} }
} }

View File

@ -127,10 +127,10 @@ namespace osu.Game.Screens.Select
config.SetValue(OsuSetting.DisplayStarsMaximum, 10.1); config.SetValue(OsuSetting.DisplayStarsMaximum, 10.1);
}); });
string lowerStar = filter.UserStarDifficulty.Min == null ? "∞" : $"{filter.UserStarDifficulty.Min:N1}"; string lowerStar = $"{filter.UserStarDifficulty.Min ?? 0:N1}";
string upperStar = filter.UserStarDifficulty.Max == null ? "∞" : $"{filter.UserStarDifficulty.Max:N1}"; string upperStar = filter.UserStarDifficulty.Max == null ? "∞" : $"{filter.UserStarDifficulty.Max:N1}";
textFlow.AddText($" the {lowerStar}-{upperStar} star difficulty filter."); textFlow.AddText($" the {lowerStar} - {upperStar} star difficulty filter.");
} }
// TODO: Add realm queries to hint at which ruleset results are available in (and allow clicking to switch). // TODO: Add realm queries to hint at which ruleset results are available in (and allow clicking to switch).

View File

@ -11,9 +11,12 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Screens; using osu.Game.Screens;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using osuTK.Input;
namespace osu.Game.Skinning.Editor namespace osu.Game.Skinning.Editor
{ {
@ -90,6 +93,47 @@ namespace osu.Game.Skinning.Editor
base.AddBlueprintFor(item); base.AddBlueprintFor(item);
} }
protected override bool OnKeyDown(KeyDownEvent e)
{
switch (e.Key)
{
case Key.Left:
moveSelection(new Vector2(-1, 0));
return true;
case Key.Right:
moveSelection(new Vector2(1, 0));
return true;
case Key.Up:
moveSelection(new Vector2(0, -1));
return true;
case Key.Down:
moveSelection(new Vector2(0, 1));
return true;
}
return false;
}
/// <summary>
/// Move the current selection spatially by the specified delta, in screen coordinates (ie. the same coordinates as the blueprints).
/// </summary>
/// <param name="delta"></param>
private void moveSelection(Vector2 delta)
{
var firstBlueprint = SelectionHandler.SelectedBlueprints.FirstOrDefault();
if (firstBlueprint == null)
return;
// convert to game space coordinates
delta = firstBlueprint.ToScreenSpace(delta) - firstBlueprint.ToScreenSpace(Vector2.Zero);
SelectionHandler.HandleMovement(new MoveSelectionEvent<ISkinnableDrawable>(firstBlueprint, delta));
}
protected override SelectionHandler<ISkinnableDrawable> CreateSelectionHandler() => new SkinSelectionHandler(); protected override SelectionHandler<ISkinnableDrawable> CreateSelectionHandler() => new SkinSelectionHandler();
protected override SelectionBlueprint<ISkinnableDrawable> CreateBlueprintFor(ISkinnableDrawable component) protected override SelectionBlueprint<ISkinnableDrawable> CreateBlueprintFor(ISkinnableDrawable component)

View File

@ -81,13 +81,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void Disconnect() => isConnected.Value = false; public void Disconnect() => isConnected.Value = false;
public MultiplayerRoomUser AddUser(APIUser user, bool markAsPlaying = false) public MultiplayerRoomUser AddUser(APIUser user, bool markAsPlaying = false)
{ => AddUser(new MultiplayerRoomUser(user.Id) { User = user }, markAsPlaying);
var roomUser = new MultiplayerRoomUser(user.Id) { User = user };
public MultiplayerRoomUser AddUser(MultiplayerRoomUser roomUser, bool markAsPlaying = false)
{
addUser(roomUser); addUser(roomUser);
if (markAsPlaying) if (markAsPlaying)
PlayingUserIds.Add(user.Id); PlayingUserIds.Add(roomUser.UserID);
return roomUser; return roomUser;
} }

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -37,6 +38,7 @@ namespace osu.Game.Tests.Visual.Spectator
private readonly Dictionary<int, ReplayFrame> lastReceivedUserFrames = new Dictionary<int, ReplayFrame>(); private readonly Dictionary<int, ReplayFrame> lastReceivedUserFrames = new Dictionary<int, ReplayFrame>();
private readonly Dictionary<int, int> userBeatmapDictionary = new Dictionary<int, int>(); private readonly Dictionary<int, int> userBeatmapDictionary = new Dictionary<int, int>();
private readonly Dictionary<int, APIMod[]> userModsDictionary = new Dictionary<int, APIMod[]>();
private readonly Dictionary<int, int> userNextFrameDictionary = new Dictionary<int, int>(); private readonly Dictionary<int, int> userNextFrameDictionary = new Dictionary<int, int>();
[Resolved] [Resolved]
@ -52,9 +54,11 @@ namespace osu.Game.Tests.Visual.Spectator
/// </summary> /// </summary>
/// <param name="userId">The user to start play for.</param> /// <param name="userId">The user to start play for.</param>
/// <param name="beatmapId">The playing beatmap id.</param> /// <param name="beatmapId">The playing beatmap id.</param>
public void SendStartPlay(int userId, int beatmapId) /// <param name="mods">The mods the user has applied.</param>
public void SendStartPlay(int userId, int beatmapId, APIMod[]? mods = null)
{ {
userBeatmapDictionary[userId] = beatmapId; userBeatmapDictionary[userId] = beatmapId;
userModsDictionary[userId] = mods ?? Array.Empty<APIMod>();
userNextFrameDictionary[userId] = 0; userNextFrameDictionary[userId] = 0;
sendPlayingState(userId); sendPlayingState(userId);
} }
@ -73,10 +77,12 @@ namespace osu.Game.Tests.Visual.Spectator
{ {
BeatmapID = userBeatmapDictionary[userId], BeatmapID = userBeatmapDictionary[userId],
RulesetID = 0, RulesetID = 0,
Mods = userModsDictionary[userId],
State = state State = state
}); });
userBeatmapDictionary.Remove(userId); userBeatmapDictionary.Remove(userId);
userModsDictionary.Remove(userId);
} }
/// <summary> /// <summary>
@ -125,6 +131,7 @@ namespace osu.Game.Tests.Visual.Spectator
// Track the local user's playing beatmap ID. // Track the local user's playing beatmap ID.
Debug.Assert(state.BeatmapID != null); Debug.Assert(state.BeatmapID != null);
userBeatmapDictionary[api.LocalUser.Value.Id] = state.BeatmapID.Value; userBeatmapDictionary[api.LocalUser.Value.Id] = state.BeatmapID.Value;
userModsDictionary[api.LocalUser.Value.Id] = state.Mods.ToArray();
return ((ISpectatorClient)this).UserBeganPlaying(api.LocalUser.Value.Id, state); return ((ISpectatorClient)this).UserBeganPlaying(api.LocalUser.Value.Id, state);
} }
@ -158,6 +165,7 @@ namespace osu.Game.Tests.Visual.Spectator
{ {
BeatmapID = userBeatmapDictionary[userId], BeatmapID = userBeatmapDictionary[userId],
RulesetID = 0, RulesetID = 0,
Mods = userModsDictionary[userId],
State = SpectatedUserState.Playing State = SpectatedUserState.Playing
}); });
} }

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.15.1" /> <PackageReference Include="Realm" Version="10.15.1" />
<PackageReference Include="ppy.osu.Framework" Version="2022.901.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.908.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.831.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.831.0" />
<PackageReference Include="Sentry" Version="3.20.1" /> <PackageReference Include="Sentry" Version="3.20.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" /> <PackageReference Include="SharpCompress" Version="0.32.2" />

View File

@ -61,7 +61,7 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.901.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2022.908.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.831.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.831.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
@ -84,7 +84,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2022.901.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.908.0" />
<PackageReference Include="SharpCompress" Version="0.32.1" /> <PackageReference Include="SharpCompress" Version="0.32.1" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />