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>
<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 Label="Transitive Dependencies">
<!-- 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
using System.ComponentModel;
using osu.Framework.Allocation;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Catch
{
[Cached]
public class CatchInputManager : RulesetInputManager<CatchAction>
{
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
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Input;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
@ -32,6 +33,12 @@ namespace osu.Game.Rulesets.Catch.UI
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 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.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Framework.Configuration.Tracking;
using osu.Game.Configuration;
@ -33,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
scrollTime => new SettingDescription(
rawValue: scrollTime,
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.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
@ -34,7 +33,7 @@ namespace osu.Game.Rulesets.Mania
LabelText = "Scrolling direction",
Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection)
},
new SettingsSlider<double, TimeSlider>
new SettingsSlider<double, ManiaScrollSlider>
{
LabelText = "Scroll speed",
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.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Localisation;
using osu.Framework.Utils;
@ -9,6 +10,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
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 Random? rng;
private Random random = null!;
public void ApplyToBeatmap(IBeatmap beatmap)
{
if (!(beatmap is OsuBeatmap osuBeatmap))
if (beatmap is not OsuBeatmap osuBeatmap)
return;
Seed.Value ??= RNG.Next();
rng = new Random((int)Seed.Value);
random = new Random((int)Seed.Value);
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
// to prevent shaky-line-shaped streams
if (positionInfo.HitObject.IndexInCurrentCombo % 5 == 0)
rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1;
if (positionInfo == positionInfos.First())
if (shouldStartNewSection(osuBeatmap, positionInfos, i))
{
positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2);
positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
sectionOffset = OsuHitObjectGenerationUtils.RandomGaussian(random, 0, 0.0008f);
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
{
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);
}
/// <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;
// rate has to be applied each frame, because it's not guaranteed to be constant throughout playback
// (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)

View File

@ -8,6 +8,7 @@ using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
@ -186,5 +187,39 @@ namespace osu.Game.Rulesets.Osu.Utils
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.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Timing;
using osu.Game.Screens.Play;
@ -13,21 +14,20 @@ namespace osu.Game.Tests.NonVisual
{
[TestCase(0)]
[TestCase(1)]
public void TestTrueGameplayRateWithZeroAdjustment(double underlyingClockRate)
public void TestTrueGameplayRateWithGameplayAdjustment(double underlyingClockRate)
{
var framedClock = new FramedClock(new ManualClock { Rate = underlyingClockRate });
var gameplayClock = new TestGameplayClockContainer(framedClock);
Assert.That(gameplayClock.TrueGameplayRate, Is.EqualTo(0));
Assert.That(gameplayClock.GetTrueGameplayRate(), Is.EqualTo(2));
}
private class TestGameplayClockContainer : GameplayClockContainer
{
public override IEnumerable<double> NonGameplayAdjustments => new[] { 0.0 };
public TestGameplayClockContainer(IFrameBasedClock 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-classic-20220723.osk",
// 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>

View File

@ -15,12 +15,14 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Beatmaps.Drawables.Cards.Buttons;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Beatmaps
{
@ -295,5 +297,22 @@ namespace osu.Game.Tests.Visual.Beatmaps
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.Drawables.Cards.Buttons;
using osu.Game.Configuration;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Resources.Localisation.Web;
@ -58,6 +59,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
State = { Value = DownloadState.NotDownloaded },
Scale = new Vector2(2)
};
});

View File

@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
};
});
AddStep("enable dim", () => thumbnail.Dimmed.Value = true);
AddUntilStep("button visible", () => playButton.IsPresent);
AddUntilStep("button visible", () => playButton.Alpha == 1);
AddStep("click button", () =>
{
@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
AddStep("disable dim", () => thumbnail.Dimmed.Value = false);
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.
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("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)));

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("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)));
AddAssert("zero width", () => sprites.All(s => s.ScreenSpaceDrawQuad.Width == 0));
AddAssert("sprites not present", () => sprites.All(s => !s.IsPresent));
}
[Test]

View File

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

View File

@ -81,9 +81,11 @@ namespace osu.Game.Tests.Visual.Gameplay
CreateTest();
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));
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]

View File

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

View File

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

View File

@ -91,8 +91,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
break;
case StopCountdownRequest:
multiplayerRoom.Countdown = null;
raiseRoomUpdated();
clearRoomCountdown();
break;
}
});
@ -244,14 +243,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
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));
AddUntilStep("local user is host", () => multiplayerRoom.Host?.Equals(multiplayerClient.Object.LocalUser) == true);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
checkLocalUserState(MultiplayerUserState.Ready);
AddAssert("countdown still active", () => multiplayerRoom.Countdown != null);
AddAssert("countdown still active", () => multiplayerRoom.ActiveCountdowns.Any());
}
[Test]
@ -392,7 +391,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
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();
}

View File

@ -13,9 +13,11 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
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);
}
[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]
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[] userIds, int? beatmapId = null)
private void start(int[] userIds, int? beatmapId = null, APIMod[]? mods = null)
{
AddStep("start play", () =>
{
@ -429,10 +443,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
var user = new MultiplayerRoomUser(id)
{
User = new APIUser { Id = id },
Mods = mods ?? Array.Empty<APIMod>(),
};
OnlinePlayDependencies.MultiplayerClient.AddUser(user.User, true);
SpectatorClient.SendStartPlay(id, beatmapId ?? importedBeatmapId);
OnlinePlayDependencies.MultiplayerClient.AddUser(user, true);
SpectatorClient.SendStartPlay(id, beatmapId ?? importedBeatmapId, mods);
playingUsers.Add(user);
}

View File

@ -19,7 +19,6 @@ using osu.Game.Database;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
@ -68,37 +67,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
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]
public void TestBeatmapConfirmed()
{
@ -152,8 +120,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
public new BeatmapCarousel Carousel => base.Carousel;
public TestMultiplayerMatchSongSelect(Room room, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null)
: base(room, null, beatmap, ruleset)
public TestMultiplayerMatchSongSelect(Room room)
: 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()
{

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
@ -20,7 +18,7 @@ namespace osu.Game.Tests.Visual.Ranking
{
public class TestSceneHitEventTimingDistributionGraph : OsuTestScene
{
private HitEventTimingDistributionGraph graph;
private HitEventTimingDistributionGraph graph = null!;
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());
}
[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]
public void TestZeroTimeOffset()
{

View File

@ -1,10 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
@ -13,6 +12,7 @@ using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Platform;
using osu.Framework.Screens;
@ -46,11 +46,12 @@ namespace osu.Game.Tests.Visual.SongSelect
[TestFixture]
public class TestScenePlaySongSelect : ScreenTestScene
{
private BeatmapManager manager;
private RulesetStore rulesets;
private MusicController music;
private WorkingBeatmap defaultBeatmap;
private TestSongSelect songSelect;
private BeatmapManager manager = null!;
private RulesetStore rulesets = null!;
private MusicController music = null!;
private WorkingBeatmap defaultBeatmap = null!;
private OsuConfigManager config = null!;
private TestSongSelect? songSelect;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
@ -69,8 +70,6 @@ namespace osu.Game.Tests.Visual.SongSelect
Dependencies.Cache(config = new OsuConfigManager(LocalStorage));
}
private OsuConfigManager config;
public override void SetUpSteps()
{
base.SetUpSteps();
@ -85,7 +84,7 @@ namespace osu.Game.Tests.Visual.SongSelect
songSelect = null;
});
AddStep("delete all beatmaps", () => manager?.Delete());
AddStep("delete all beatmaps", () => manager.Delete());
}
[Test]
@ -98,7 +97,7 @@ namespace osu.Game.Tests.Visual.SongSelect
addRulesetImportStep(0);
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);
}
@ -144,7 +143,7 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect();
AddAssert("filter count is 1", () => songSelect.FilterCount == 1);
AddAssert("filter count is 1", () => songSelect?.FilterCount == 1);
}
[Test]
@ -156,7 +155,7 @@ namespace osu.Game.Tests.Visual.SongSelect
waitForInitialSelection();
WorkingBeatmap selected = null;
WorkingBeatmap? selected = null;
AddStep("store selected beatmap", () => selected = Beatmap.Value);
@ -166,7 +165,7 @@ namespace osu.Game.Tests.Visual.SongSelect
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);
}
@ -179,7 +178,7 @@ namespace osu.Game.Tests.Visual.SongSelect
waitForInitialSelection();
WorkingBeatmap selected = null;
WorkingBeatmap? selected = null;
AddStep("store selected beatmap", () => selected = Beatmap.Value);
@ -189,7 +188,7 @@ namespace osu.Game.Tests.Visual.SongSelect
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);
}
@ -202,23 +201,23 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault);
WorkingBeatmap selected = null;
WorkingBeatmap? selected = null;
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", () =>
{
InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmap>()
.First(b => !((CarouselBeatmap)b.Item).BeatmapInfo.Equals(songSelect.Carousel.SelectedBeatmapInfo)));
InputManager.MoveMouseTo(songSelect!.Carousel.ChildrenOfType<DrawableCarouselBeatmap>()
.First(b => !((CarouselBeatmap)b.Item).BeatmapInfo.Equals(songSelect!.Carousel.SelectedBeatmapInfo)));
InputManager.Click(MouseButton.Left);
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);
}
@ -231,14 +230,14 @@ namespace osu.Game.Tests.Visual.SongSelect
waitForInitialSelection();
WorkingBeatmap selected = null;
WorkingBeatmap? selected = null;
AddStep("store selected beatmap", () => selected = Beatmap.Value);
AddStep("select next and enter", () =>
{
InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmap>()
.First(b => !((CarouselBeatmap)b.Item).BeatmapInfo.Equals(songSelect.Carousel.SelectedBeatmapInfo)));
InputManager.MoveMouseTo(songSelect!.Carousel.ChildrenOfType<DrawableCarouselBeatmap>()
.First(b => !((CarouselBeatmap)b.Item).BeatmapInfo.Equals(songSelect!.Carousel.SelectedBeatmapInfo)));
InputManager.PressButton(MouseButton.Left);
@ -247,7 +246,7 @@ namespace osu.Game.Tests.Visual.SongSelect
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);
}
@ -260,11 +259,11 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect();
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());
AddUntilStep("wait for current", () => songSelect.IsCurrentScreen());
AddAssert("filter count is 1", () => songSelect.FilterCount == 1);
AddStep("return", () => songSelect!.MakeCurrent());
AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen());
AddAssert("filter count is 1", () => songSelect!.FilterCount == 1);
}
[Test]
@ -278,13 +277,13 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect();
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("return", () => songSelect.MakeCurrent());
AddUntilStep("wait for current", () => songSelect.IsCurrentScreen());
AddAssert("filter count is 2", () => songSelect.FilterCount == 2);
AddStep("return", () => songSelect!.MakeCurrent());
AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen());
AddAssert("filter count is 2", () => songSelect!.FilterCount == 2);
}
[Test]
@ -295,7 +294,7 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect();
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", () =>
{
@ -304,9 +303,9 @@ namespace osu.Game.Tests.Visual.SongSelect
Beatmap.Value = manager.GetWorkingBeatmap(anotherBeatmap);
});
AddStep("return", () => songSelect.MakeCurrent());
AddUntilStep("wait for current", () => songSelect.IsCurrentScreen());
AddAssert("carousel updated", () => songSelect.Carousel.SelectedBeatmapInfo.Equals(Beatmap.Value.BeatmapInfo));
AddStep("return", () => songSelect!.MakeCurrent());
AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen());
AddAssert("carousel updated", () => songSelect!.Carousel.SelectedBeatmapInfo?.Equals(Beatmap.Value.BeatmapInfo) == true);
}
[Test]
@ -318,15 +317,15 @@ namespace osu.Game.Tests.Visual.SongSelect
addRulesetImportStep(0);
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);
AddStep("manual pause", () => music.TogglePause());
checkMusicPlaying(false);
AddStep("select next difficulty", () => songSelect.Carousel.SelectNext(skipDifficulties: false));
AddStep("select next difficulty", () => songSelect!.Carousel.SelectNext(skipDifficulties: false));
checkMusicPlaying(false);
AddStep("select next set", () => songSelect.Carousel.SelectNext());
AddStep("select next set", () => songSelect!.Carousel.SelectNext());
checkMusicPlaying(true);
}
@ -366,13 +365,13 @@ namespace osu.Game.Tests.Visual.SongSelect
public void TestDummy()
{
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();
AddUntilStep("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap);
AddUntilStep("random map selected", () => songSelect!.CurrentBeatmap != defaultBeatmap);
}
[Test]
@ -381,7 +380,7 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect();
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 Title", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title));
@ -398,7 +397,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
createSongSelect();
addRulesetImportStep(2);
AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null);
AddUntilStep("no selection", () => songSelect!.Carousel.SelectedBeatmapInfo == null);
}
[Test]
@ -408,13 +407,13 @@ namespace osu.Game.Tests.Visual.SongSelect
changeRuleset(2);
addRulesetImportStep(2);
addRulesetImportStep(1);
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.Ruleset.OnlineID == 2);
AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.Ruleset.OnlineID == 2);
changeRuleset(1);
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.Ruleset.OnlineID == 1);
AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.Ruleset.OnlineID == 1);
changeRuleset(0);
AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null);
AddUntilStep("no selection", () => songSelect!.Carousel.SelectedBeatmapInfo == null);
}
[Test]
@ -423,7 +422,7 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect();
changeRuleset(0);
Live<BeatmapSetInfo> original = null!;
Live<BeatmapSetInfo>? original = null;
int originalOnlineSetID = 0;
AddStep(@"Sort by artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist));
@ -431,12 +430,17 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("import original", () =>
{
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.
AddStep("Update original with bogus info", () =>
{
Debug.Assert(original != null);
original.PerformWrite(set =>
{
foreach (var beatmap in set.Beatmaps)
@ -457,13 +461,19 @@ namespace osu.Game.Tests.Visual.SongSelect
manager.Import(testBeatmapSetInfo);
}, 10);
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID);
AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID);
Task<Live<BeatmapSetInfo>> updateTask = null!;
AddStep("update beatmap", () => updateTask = manager.ImportAsUpdate(new ProgressNotification(), new ImportTask(TestResources.GetQuickTestBeatmapForImport()), original.Value));
Task<Live<BeatmapSetInfo>?> updateTask = null!;
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("retained selection", () => songSelect.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID);
AddUntilStep("retained selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID);
}
[Test]
@ -473,13 +483,13 @@ namespace osu.Game.Tests.Visual.SongSelect
changeRuleset(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);
BeatmapInfo target = null;
BeatmapInfo? target = null;
AddStep("select beatmap/ruleset externally", () =>
{
@ -490,10 +500,10 @@ namespace osu.Game.Tests.Visual.SongSelect
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
AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo.MatchesOnlineID(target));
AddUntilStep("selection shown on wedge", () => songSelect!.CurrentBeatmapDetailsBeatmap.BeatmapInfo.MatchesOnlineID(target));
}
[Test]
@ -503,13 +513,13 @@ namespace osu.Game.Tests.Visual.SongSelect
changeRuleset(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);
BeatmapInfo target = null;
BeatmapInfo? target = null;
AddStep("select beatmap/ruleset externally", () =>
{
@ -520,12 +530,12 @@ namespace osu.Game.Tests.Visual.SongSelect
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);
// 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]
@ -543,12 +553,12 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("change ruleset", () =>
{
SelectedMods.ValueChanged += onModChange;
songSelect.Ruleset.ValueChanged += onRulesetChange;
songSelect!.Ruleset.ValueChanged += onRulesetChange;
Ruleset.Value = new TaikoRuleset().RulesetInfo;
SelectedMods.ValueChanged -= onModChange;
songSelect.Ruleset.ValueChanged -= onRulesetChange;
songSelect!.Ruleset.ValueChanged -= onRulesetChange;
});
AddAssert("mods changed before ruleset", () => modChangeIndex < rulesetChangeIndex);
@ -579,18 +589,18 @@ namespace osu.Game.Tests.Visual.SongSelect
{
createSongSelect();
addManyTestMaps();
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null);
AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo != null);
bool startRequested = false;
AddStep("set filter and finalize", () =>
{
songSelect.StartRequested = () => startRequested = true;
songSelect!.StartRequested = () => startRequested = true;
songSelect.Carousel.Filter(new FilterCriteria { SearchText = "somestringthatshouldn'tbematchable" });
songSelect.FinaliseSelection();
songSelect!.Carousel.Filter(new FilterCriteria { SearchText = "somestringthatshouldn'tbematchable" });
songSelect!.FinaliseSelection();
songSelect.StartRequested = null;
songSelect!.StartRequested = null;
});
AddAssert("start not requested", () => !startRequested);
@ -610,15 +620,15 @@ namespace osu.Game.Tests.Visual.SongSelect
// used for filter check below
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("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;
@ -632,24 +642,24 @@ namespace osu.Game.Tests.Visual.SongSelect
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)", () =>
{
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.
return selectedPanel.ChildrenOfType<FilterableDifficultyIcon>().All(i =>
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));
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("carousel still correct", () => songSelect.Carousel.SelectedBeatmapInfo.MatchesOnlineID(target));
AddAssert("carousel still correct", () => songSelect!.Carousel.SelectedBeatmapInfo.MatchesOnlineID(target));
}
[Test]
@ -662,15 +672,15 @@ namespace osu.Game.Tests.Visual.SongSelect
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("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", () =>
{
@ -682,15 +692,15 @@ namespace osu.Game.Tests.Visual.SongSelect
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));
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);
AddAssert("carousel lost selection", () => songSelect.Carousel.SelectedBeatmapInfo == null);
AddAssert("carousel lost selection", () => songSelect!.Carousel.SelectedBeatmapInfo == null);
}
[Test]
@ -711,11 +721,11 @@ namespace osu.Game.Tests.Visual.SongSelect
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]
@ -738,11 +748,11 @@ namespace osu.Game.Tests.Visual.SongSelect
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]
@ -765,11 +775,11 @@ namespace osu.Game.Tests.Visual.SongSelect
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]
@ -778,10 +788,10 @@ namespace osu.Game.Tests.Visual.SongSelect
Guid? previousID = null;
createSongSelect();
addRulesetImportStep(0);
AddStep("Move to last difficulty", () => songSelect.Carousel.SelectBeatmap(songSelect.Carousel.BeatmapSets.First().Beatmaps.Last()));
AddStep("Store current ID", () => previousID = songSelect.Carousel.SelectedBeatmapInfo.ID);
AddStep("Hide first beatmap", () => manager.Hide(songSelect.Carousel.SelectedBeatmapSet.Beatmaps.First()));
AddAssert("Selected beatmap has not changed", () => songSelect.Carousel.SelectedBeatmapInfo.ID == previousID);
AddStep("Move to last difficulty", () => songSelect!.Carousel.SelectBeatmap(songSelect!.Carousel.BeatmapSets.First().Beatmaps.Last()));
AddStep("Store current ID", () => previousID = songSelect!.Carousel.SelectedBeatmapInfo!.ID);
AddStep("Hide first beatmap", () => manager.Hide(songSelect!.Carousel.SelectedBeatmapSet!.Beatmaps.First()));
AddAssert("Selected beatmap has not changed", () => songSelect!.Carousel.SelectedBeatmapInfo?.ID == previousID);
}
[Test]
@ -792,17 +802,24 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for selection", () => !Beatmap.IsDefault);
DrawableCarouselBeatmapSet set = null;
DrawableCarouselBeatmapSet set = null!;
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", () =>
{
return (difficultyIcon = set.ChildrenOfType<FilterableDifficultyIcon>()
.FirstOrDefault(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex())) != null;
var foundIcon = set.ChildrenOfType<FilterableDifficultyIcon>()
.FirstOrDefault(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex());
if (foundIcon == null)
return false;
difficultyIcon = foundIcon;
return true;
});
AddStep("Click on a difficulty", () =>
@ -815,21 +832,24 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("Selected beatmap correct", () => getCurrentBeatmapIndex() == getDifficultyIconIndex(set, difficultyIcon));
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>
{
Min = maxBPM = songSelect.Carousel.SelectedBeatmapSet.MaxBPM,
Min = maxBPM = songSelect!.Carousel.SelectedBeatmapSet!.MaxBPM,
IsLowerInclusive = true
}
}));
BeatmapInfo filteredBeatmap = null;
FilterableDifficultyIcon filteredIcon = null;
BeatmapInfo? filteredBeatmap = null;
FilterableDifficultyIcon? filteredIcon = null;
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);
int filteredBeatmapIndex = getBeatmapIndex(selectedSet, filteredBeatmap);
filteredIcon = set.ChildrenOfType<FilterableDifficultyIcon>().ElementAt(filteredBeatmapIndex);
@ -842,7 +862,7 @@ namespace osu.Game.Tests.Visual.SongSelect
InputManager.Click(MouseButton.Left);
});
AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmapInfo.Equals(filteredBeatmap));
AddAssert("Selected beatmap correct", () => songSelect!.Carousel.SelectedBeatmapInfo?.Equals(filteredBeatmap) == true);
}
[Test]
@ -907,14 +927,14 @@ namespace osu.Game.Tests.Visual.SongSelect
manager.Import(TestResources.CreateTestBeatmapSetInfo(3, usableRulesets));
});
DrawableCarouselBeatmapSet set = null;
DrawableCarouselBeatmapSet? set = null;
AddUntilStep("Find the DrawableCarouselBeatmapSet", () =>
{
set = songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().FirstOrDefault();
set = songSelect!.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().FirstOrDefault();
return set != null;
});
FilterableDifficultyIcon difficultyIcon = null;
FilterableDifficultyIcon? difficultyIcon = null;
AddUntilStep("Find an icon for different ruleset", () =>
{
difficultyIcon = set.ChildrenOfType<FilterableDifficultyIcon>()
@ -937,7 +957,7 @@ namespace osu.Game.Tests.Visual.SongSelect
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);
}
@ -948,7 +968,7 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect();
BeatmapSetInfo imported = null;
BeatmapSetInfo? imported = null;
AddStep("import huge difficulty count map", () =>
{
@ -956,20 +976,27 @@ namespace osu.Game.Tests.Visual.SongSelect
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", () =>
{
set = songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().FirstOrDefault();
set = songSelect!.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().FirstOrDefault();
return set != null;
});
GroupedDifficultyIcon groupIcon = null;
GroupedDifficultyIcon groupIcon = null!;
AddUntilStep("Find group icon for different ruleset", () =>
{
return (groupIcon = set.ChildrenOfType<GroupedDifficultyIcon>()
.FirstOrDefault(icon => icon.Items.First().BeatmapInfo.Ruleset.OnlineID == 3)) != null;
var foundIcon = set.ChildrenOfType<GroupedDifficultyIcon>()
.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);
@ -1004,7 +1031,7 @@ namespace osu.Game.Tests.Visual.SongSelect
// this ruleset change should be overridden by the present.
Ruleset.Value = getSwitchBeatmap().Ruleset;
songSelect.PresentScore(new ScoreInfo
songSelect!.PresentScore(new ScoreInfo
{
User = new APIUser { Username = "woo" },
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 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.
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 ruleset is correct for score", () => Ruleset.Value.OnlineID == 0);
@ -1054,23 +1081,29 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect();
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));
AddUntilStep("mod overlay hidden", () => songSelect.ModSelect.State.Value == Visibility.Hidden);
AddUntilStep("mod overlay hidden", () => songSelect!.ModSelect.State.Value == Visibility.Hidden);
}
private void waitForInitialSelection()
{
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 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)
{
@ -1079,14 +1112,14 @@ namespace osu.Game.Tests.Visual.SongSelect
private void addRulesetImportStep(int id)
{
Live<BeatmapSetInfo> imported = null;
Live<BeatmapSetInfo>? imported = null;
AddStep($"import test map for ruleset {id}", () => imported = importForRuleset(id));
// 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.
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) =>
AddUntilStep($"music {(playing ? "" : "not ")}playing", () => music.IsPlaying == playing);
@ -1098,8 +1131,8 @@ namespace osu.Game.Tests.Visual.SongSelect
private void createSongSelect()
{
AddStep("create song select", () => LoadScreen(songSelect = new TestSongSelect()));
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen());
AddUntilStep("wait for carousel loaded", () => songSelect.Carousel.IsAlive);
AddUntilStep("wait for present", () => songSelect!.IsCurrentScreen());
AddUntilStep("wait for carousel loaded", () => songSelect!.Carousel.IsAlive);
}
/// <summary>
@ -1123,12 +1156,14 @@ namespace osu.Game.Tests.Visual.SongSelect
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
rulesets?.Dispose();
if (rulesets.IsNotNull())
rulesets.Dispose();
}
private class TestSongSelect : PlaySongSelect
{
public Action StartRequested;
public Action? StartRequested;
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>
/// 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>
public void StartMatch()
{
if (Team1.Value == null || Team2.Value == null)
return;
if (Team1Score.Value > 0 || Team2Score.Value > 0)
return;
Team1Score.Value = 0;
Team2Score.Value = 0;
}

View File

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -14,6 +15,7 @@ using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game
@ -23,6 +25,9 @@ namespace osu.Game
[Resolved]
private RulesetStore rulesetStore { get; set; } = null!;
[Resolved]
private ScoreManager scoreManager { get; set; } = null!;
[Resolved]
private RealmAccess realmAccess { get; set; } = null!;
@ -46,6 +51,7 @@ namespace osu.Game
Logger.Log("Beginning background beatmap processing..");
checkForOutdatedStarRatings();
processBeatmapSetsWithMissingMetrics();
processScoresWithMissingStatistics();
}).ContinueWith(t =>
{
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 IBindable<DownloadState> State => state;
private readonly Bindable<DownloadState> state = new Bindable<DownloadState>();
public Bindable<DownloadState> State { get; } = new Bindable<DownloadState>();
private readonly APIBeatmapSet beatmapSet;
@ -48,14 +47,19 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
{
base.LoadComplete();
preferNoVideo.BindValueChanged(_ => updateState());
state.BindValueChanged(_ => updateState(), true);
State.BindValueChanged(_ => updateState(), true);
FinishTransforms(true);
}
private void updateState()
{
switch (state.Value)
switch (State.Value)
{
case DownloadState.Unknown:
Action = null;
TooltipText = string.Empty;
break;
case DownloadState.Downloading:
case DownloadState.Importing:
Action = null;

View File

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

View File

@ -121,7 +121,18 @@ namespace osu.Game.Beatmaps
protected override void 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

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>
/// Retrieves the colour for a <see cref="HitResult"/>.
/// </summary>
public Color4 ForHitResult(HitResult judgement)
public Color4 ForHitResult(HitResult result)
{
switch (judgement)
switch (result)
{
case HitResult.Perfect:
case HitResult.Great:
return Blue;
case HitResult.Ok:
case HitResult.Good:
return Green;
case HitResult.SmallTickMiss:
case HitResult.LargeTickMiss:
case HitResult.Miss:
return Red;
case HitResult.Meh:
return Yellow;
case HitResult.Miss:
return Red;
case HitResult.Ok:
return Green;
case HitResult.Good:
return GreenLight;
case HitResult.SmallTickHit:
case HitResult.LargeTickHit:
case HitResult.Great:
return Blue;
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; }
[JsonProperty("legacy_score_id")]
public uint? LegacyScoreId { get; set; }
public ulong? LegacyScoreId { get; set; }
#region osu-web API additions (not stored to database).
[JsonProperty("id")]
public long? ID { get; set; }
public ulong? ID { get; set; }
[JsonProperty("user")]
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),
};
public long OnlineID => ID ?? -1;
public long OnlineID => (long?)ID ?? -1;
}
}

View File

@ -5,6 +5,7 @@ namespace osu.Game.Online
{
public enum DownloadState
{
Unknown,
NotDownloaded,
Downloading,
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.
using MessagePack;
using Newtonsoft.Json;
namespace osu.Game.Online.Multiplayer.Countdown
{
@ -11,5 +12,14 @@ namespace osu.Game.Online.Multiplayer.Countdown
[MessagePackObject]
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.
/// </summary>
[MessagePackObject]
public class ForceGameplayStartCountdown : MultiplayerCountdown
public sealed class ForceGameplayStartCountdown : MultiplayerCountdown
{
}
}

View File

@ -13,7 +13,8 @@ namespace osu.Game.Online.Multiplayer
[Serializable]
[MessagePackObject]
// 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
{
}

View File

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

View File

@ -552,8 +552,14 @@ namespace osu.Game.Online.Multiplayer
switch (e)
{
case CountdownChangedEvent countdownChangedEvent:
Room.Countdown = countdownChangedEvent.Countdown;
case CountdownStartedEvent countdownStartedEvent:
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;
}

View File

@ -15,13 +15,24 @@ namespace osu.Game.Online.Multiplayer
[Union(1, typeof(ForceGameplayStartCountdown))]
public abstract class MultiplayerCountdown
{
/// <summary>
/// A unique identifier for this countdown.
/// </summary>
[Key(0)]
public int ID { get; set; }
/// <summary>
/// The amount of time remaining in the countdown.
/// </summary>
/// <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>
[Key(0)]
[Key(1)]
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>();
/// <summary>
/// The currently-running countdown.
/// The currently running countdowns.
/// </summary>
[Key(7)]
public MultiplayerCountdown? Countdown { get; set; }
public IList<MultiplayerCountdown> ActiveCountdowns { get; set; } = new List<MultiplayerCountdown>();
[JsonConstructor]
[SerializationConstructor]

View File

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

View File

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

View File

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

View File

@ -390,11 +390,6 @@ namespace osu.Game
var framedClock = new FramedClock(beatmap.Track);
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()

View File

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

View File

@ -16,7 +16,6 @@ namespace osu.Game.Rulesets.Mods
public override string Acronym => "WD";
public override LocalisableString Description => "Sloooow doooown...";
public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleDown;
public override double ScoreMultiplier => 1.0;
[SettingSource("Initial rate", "The starting speed of the track")]
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 LocalisableString Description => "Can you keep up?";
public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleUp;
public override double ScoreMultiplier => 1.0;
[SettingSource("Initial rate", "The starting speed of the track")]
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.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Serialization;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Utils;
namespace osu.Game.Rulesets.Scoring
@ -135,6 +135,8 @@ namespace osu.Game.Rulesets.Scoring
#pragma warning disable CS0618
public static class HitResultExtensions
{
private static readonly IList<HitResult> order = EnumExtensions.GetValuesInOrder<HitResult>().ToList();
/// <summary>
/// Whether a <see cref="HitResult"/> increases the combo.
/// </summary>
@ -282,6 +284,13 @@ namespace osu.Game.Rulesets.Scoring
Debug.Assert(minResult <= 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
}

View File

@ -30,6 +30,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using osuTK;
namespace osu.Game.Rulesets.UI
@ -38,7 +39,7 @@ namespace osu.Game.Rulesets.UI
/// Displays an interactive ruleset gameplay instance.
/// </summary>
/// <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
{
public override event Action<JudgementResult> NewResult;
@ -338,7 +339,10 @@ namespace osu.Game.Rulesets.UI
public abstract DrawableHitObject<TObject> CreateDrawableRepresentation(TObject h);
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>
/// 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.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Input.Handlers;
using osu.Game.Screens.Play;
@ -22,7 +20,7 @@ namespace osu.Game.Rulesets.UI
/// </summary>
[Cached(typeof(IGameplayClock))]
[Cached(typeof(IFrameStableClock))]
public sealed class FrameStabilityContainer : Container, IHasReplayHandler, IFrameStableClock, IGameplayClock
public sealed class FrameStabilityContainer : Container, IHasReplayHandler, IFrameStableClock
{
public ReplayInputHandler? ReplayInputHandler { get; set; }
@ -263,27 +261,11 @@ namespace osu.Game.Rulesets.UI
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 IEnumerable<double> NonGameplayAdjustments => parentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty<double>();
private readonly AudioAdjustments gameplayAdjustments = new AudioAdjustments();
public IAdjustableAudioComponent AdjustmentsFromMods => parentGameplayClock?.AdjustmentsFromMods ?? gameplayAdjustments;
#endregion

View File

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

View File

@ -20,11 +20,12 @@ using osu.Game.Input.Bindings;
using osu.Game.Input.Handlers;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using static osu.Game.Input.Handlers.ReplayInputHandler;
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
{
public readonly KeyBindingContainer<T> KeyBindingContainer;
@ -168,7 +169,7 @@ namespace osu.Game.Rulesets.UI
.Select(action => new KeyCounterAction<T>(action)));
}
public class ActionReceptor : KeyCounterDisplay.Receptor, IKeyBindingHandler<T>
private class ActionReceptor : KeyCounterDisplay.Receptor, IKeyBindingHandler<T>
{
public ActionReceptor(KeyCounterDisplay target)
: base(target)
@ -186,6 +187,37 @@ namespace osu.Game.Rulesets.UI
#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)
=> new RulesetKeyBindingContainer(ruleset, variant, unique);
@ -221,12 +253,13 @@ namespace osu.Game.Rulesets.UI
}
/// <summary>
/// Supports attaching a <see cref="KeyCounterDisplay"/>.
/// Supports attaching various HUD pieces.
/// Keys will be populated automatically and a receptor will be injected inside.
/// </summary>
public interface ICanAttachKeyCounter
public interface ICanAttachHUDPieces
{
void Attach(KeyCounterDisplay keyCounter);
void Attach(ClicksPerSecondCalculator calculator);
}
public class RulesetInputManagerInputState<T> : InputState

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Newtonsoft.Json;
@ -16,6 +17,8 @@ using osu.Game.Scoring.Legacy;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using Realms;
namespace osu.Game.Scoring
@ -71,6 +74,8 @@ namespace osu.Game.Scoring
if (model.BeatmapInfo == null) throw new ArgumentNullException(nameof(model.BeatmapInfo));
if (model.Ruleset == null) throw new ArgumentNullException(nameof(model.Ruleset));
PopulateMaximumStatistics(model);
if (string.IsNullOrEmpty(model.StatisticsJson))
model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics);
@ -78,6 +83,68 @@ namespace osu.Game.Scoring
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)
{
base.PostImport(model, realm, batchImport);

View File

@ -28,7 +28,8 @@ namespace osu.Game.Scoring
private readonly OsuConfigManager configManager;
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)
{
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) =>
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>
public Action<IEnumerable<Live<ScoreInfo>>> PresentImport

View File

@ -561,6 +561,10 @@ namespace osu.Game.Screens.OnlinePlay
{
switch (state.NewValue)
{
case DownloadState.Unknown:
// Ignore initial state to ensure the button doesn't briefly appear.
break;
case DownloadState.LocallyAvailable:
// 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)

View File

@ -433,6 +433,9 @@ namespace osu.Game.Screens.OnlinePlay.Match
private void updateWorkingBeatmap()
{
if (SelectedItem.Value == null || !this.IsCurrentScreen())
return;
var beatmap = SelectedItem.Value?.Beatmap;
// 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);
clickOperation = ongoingOperationTracker.BeginOperation();
if (isReady() && Client.IsHost && Room.Countdown == null)
if (isReady() && Client.IsHost && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
startMatch();
else
toggleReady();
@ -140,10 +140,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void cancelCountdown()
{
if (Client.Room == null)
return;
Debug.Assert(clickOperation == null);
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()
@ -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.
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)
return;

View File

@ -4,6 +4,7 @@
#nullable disable
using System;
using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
@ -79,7 +80,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
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)
{
@ -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
{

View File

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

View File

@ -57,23 +57,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void onRoomUpdated() => Scheduler.AddOnce(() =>
{
MultiplayerCountdown newCountdown;
switch (room?.Countdown)
{
case MatchStartCountdown:
newCountdown = room.Countdown;
break;
// Clear the countdown with any other (including non-null) countdown values.
default:
newCountdown = null;
break;
}
MultiplayerCountdown newCountdown = room?.ActiveCountdowns.SingleOrDefault(c => c is MatchStartCountdown);
if (newCountdown != countdown)
{
countdown = room?.Countdown;
countdown = newCountdown;
countdownChangeTime = Time.Current;
}
@ -213,7 +201,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
case MultiplayerUserState.Spectating:
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();
else
setYellow();
@ -248,8 +236,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
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 base.TooltipText;
}

View File

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

View File

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

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Game.Beatmaps;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
@ -13,6 +14,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// </summary>
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;
/// <summary>
@ -53,6 +60,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
}
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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -43,6 +44,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
[Resolved]
private MultiplayerClient multiplayerClient { get; set; } = null!;
private IAggregateAudioAdjustment? boundAdjustments;
private readonly PlayerArea[] instances;
private MasterGameplayClockContainer masterClockContainer = null!;
private SpectatorSyncManager syncManager = null!;
@ -157,6 +160,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
base.LoadComplete();
masterClockContainer.Reset();
// Start with adjustments from the first player to keep a sane state.
bindAudioAdjustments(instances.First());
}
protected override void Update()
@ -169,11 +175,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
.OrderBy(i => Math.Abs(i.SpectatorPlayerClock.CurrentTime - syncManager.CurrentMasterTime))
.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)
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)
=> clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames;

View File

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

View File

@ -1,13 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Humanizer;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -35,32 +33,34 @@ namespace osu.Game.Screens.OnlinePlay
public override bool AllowEditing => false;
[Resolved(typeof(Room), nameof(Room.Playlist))]
protected BindableList<PlaylistItem> Playlist { get; private set; }
[CanBeNull]
[Resolved(CanBeNull = true)]
protected IBindable<PlaylistItem> SelectedItem { get; private set; }
protected BindableList<PlaylistItem> Playlist { get; private set; } = null!;
[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 readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
private readonly Room room;
private WorkingBeatmap initialBeatmap;
private RulesetInfo initialRuleset;
private IReadOnlyList<Mod> initialMods;
private bool itemSelected;
private readonly PlaylistItem? initialItem;
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.initialItem = initialItem ?? room.Playlist.LastOrDefault();
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
@ -75,11 +75,6 @@ namespace osu.Game.Screens.OnlinePlay
private void load()
{
LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT };
initialBeatmap = Beatmap.Value;
initialRuleset = Ruleset.Value;
initialMods = Mods.Value.ToList();
LoadComponent(freeModSelectOverlay);
}
@ -87,14 +82,35 @@ namespace osu.Game.Screens.OnlinePlay
{
base.LoadComplete();
var rulesetInstance = SelectedItem?.Value?.RulesetID == null ? null : rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance();
if (rulesetInstance != null)
if (initialItem != 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 = SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
FreeMods.Value = SelectedItem.Value.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
// Prefer using a local databased beatmap lookup since OnlineId may be -1 for an invalid beatmap selection.
BeatmapInfo? beatmapInfo = initialItem.Beatmap as BeatmapInfo;
// 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);
@ -125,13 +141,7 @@ namespace osu.Game.Screens.OnlinePlay
AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray()
};
if (SelectItem(item))
{
itemSelected = true;
return true;
}
return false;
return SelectItem(item);
}
/// <summary>
@ -154,15 +164,7 @@ namespace osu.Game.Screens.OnlinePlay
public override bool OnExiting(ScreenExitEvent e)
{
if (!itemSelected)
{
Beatmap.Value = initialBeatmap;
Ruleset.Value = initialRuleset;
Mods.Value = initialMods;
}
freeModSelectOverlay.Hide();
return base.OnExiting(e);
}
@ -199,7 +201,6 @@ namespace osu.Game.Screens.OnlinePlay
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
freeModSelectOverlayRegistration?.Dispose();
}
}

View File

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

View File

@ -2,15 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
namespace osu.Game.Screens.Play
@ -46,7 +44,7 @@ namespace osu.Game.Screens.Play
/// </remarks>
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);
@ -196,7 +194,9 @@ namespace osu.Game.Screens.Play
void IAdjustableClock.Reset() => Reset();
public void ResetSpeedAdjustments() => throw new NotImplementedException();
public virtual void ResetSpeedAdjustments()
{
}
double IAdjustableClock.Rate
{
@ -222,23 +222,5 @@ namespace osu.Game.Screens.Play
public double FramesPerSecond => GameplayClock.FramesPerSecond;
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
{
private const float info_height = 20;
private const float bottom_bar_height = 5;
private const float graph_height = SquareGraph.Column.WIDTH * 6;
private const float handle_height = 18;
@ -65,7 +64,6 @@ namespace osu.Game.Screens.Play.HUD
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = info_height,
},
graph = new SongProgressGraph
{
@ -178,7 +176,7 @@ namespace osu.Game.Screens.Play.HUD
protected override void 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()

View File

@ -59,30 +59,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
protected Color4 GetColourForHitResult(HitResult result)
{
switch (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;
}
return colours.ForHitResult(result);
}
/// <summary>

View File

@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using System;
@ -14,9 +15,9 @@ namespace osu.Game.Screens.Play.HUD
{
public class SongProgressInfo : Container
{
private OsuSpriteText timeCurrent;
private OsuSpriteText timeLeft;
private OsuSpriteText progress;
private SizePreservingSpriteText timeCurrent;
private SizePreservingSpriteText timeLeft;
private SizePreservingSpriteText progress;
private double startTime;
private double endTime;
@ -46,36 +47,71 @@ namespace osu.Game.Screens.Play.HUD
if (clock != null)
gameplayClock = clock;
AutoSizeAxes = Axes.Y;
Children = new Drawable[]
{
timeCurrent = new OsuSpriteText
new Container
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
Colour = colours.BlueLighter,
Font = OsuFont.Numeric,
Margin = new MarginPadding
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Child = new UprightAspectMaintainingContainer
{
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,
Anchor = Anchor.BottomCentre,
Colour = colours.BlueLighter,
Font = OsuFont.Numeric,
},
timeLeft = new OsuSpriteText
{
Origin = Anchor.BottomRight,
Anchor = Anchor.BottomRight,
Colour = colours.BlueLighter,
Font = OsuFont.Numeric,
Margin = new MarginPadding
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Child = new UprightAspectMaintainingContainer
{
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.UI;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using osu.Game.Skinning;
using osuTK;
@ -49,6 +50,9 @@ namespace osu.Game.Screens.Play
public readonly HoldForMenuButton HoldToQuit;
public readonly PlayerSettingsOverlay PlayerSettingsOverlay;
[Cached]
private readonly ClicksPerSecondCalculator clicksPerSecondCalculator;
public Bindable<bool> ShowHealthBar = new Bindable<bool>(true);
private readonly DrawableRuleset drawableRuleset;
@ -122,7 +126,8 @@ namespace osu.Game.Screens.Play
KeyCounter = CreateKeyCounter(),
HoldToQuit = CreateHoldForMenuButton(),
}
}
},
clicksPerSecondCalculator = new ClicksPerSecondCalculator()
};
}
@ -259,7 +264,11 @@ namespace osu.Game.Screens.Play
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);
}

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Timing;
@ -9,12 +9,6 @@ namespace osu.Game.Screens.Play
{
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>
/// The time from which the clock should start. Will be seeked to on calling <see cref="GameplayClockContainer.Reset"/>.
/// </summary>
@ -25,9 +19,9 @@ namespace osu.Game.Screens.Play
double StartTime { get; }
/// <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>
IEnumerable<double> NonGameplayAdjustments { get; }
IAdjustableAudioComponent AdjustmentsFromMods { get; }
IBindable<bool> IsPaused { get; }
}

View File

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

View File

@ -996,12 +996,8 @@ namespace osu.Game.Screens.Play
foreach (var mod in GameplayState.Mods.OfType<IApplicableToHUD>())
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>())
mod.ApplyToTrack(musicController.CurrentTrack);
mod.ApplyToTrack(GameplayClockContainer.AdjustmentsFromMods);
updateGameplayState();
@ -1053,6 +1049,7 @@ namespace osu.Game.Screens.Play
musicController.ResetTrackAdjustments();
fadeOut();
return base.OnExiting(e);
}

View File

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

View File

@ -7,7 +7,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
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();
}
private int[] bins;
private IDictionary<HitResult, int>[] bins;
private double binSize;
private double hitOffset;
@ -69,7 +68,7 @@ namespace osu.Game.Screens.Ranking.Statistics
if (hitEvents == null || hitEvents.Count == 0)
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);
@ -89,7 +88,8 @@ namespace osu.Game.Screens.Ranking.Statistics
{
bool roundUp = true;
Array.Clear(bins, 0, bins.Length);
foreach (var bin in bins)
bin.Clear();
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.
if (index >= 0 && index < bins.Length)
bins[index]++;
{
bins[index].TryGetValue(e.Result, out int value);
bins[index][e.Result] = ++value;
}
}
if (barDrawables != null)
{
for (int i = 0; i < barDrawables.Length; i++)
{
barDrawables[i].UpdateOffset(bins[i]);
barDrawables[i].UpdateOffset(bins[i].Sum(b => b.Value));
}
}
else
{
int maxCount = bins.Max();
barDrawables = new Bar[total_timing_distribution_bins];
for (int i = 0; i < barDrawables.Length; i++)
barDrawables[i] = new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index);
int maxCount = bins.Max(b => b.Values.Sum());
barDrawables = bins.Select((bin, i) => new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index)).ToArray();
Container axisFlow;
@ -209,50 +209,97 @@ namespace osu.Game.Screens.Ranking.Statistics
private class Bar : CompositeDrawable
{
private readonly float value;
private readonly float maxValue;
private float totalValue => values.Sum(v => v.Value);
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 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.isCentre = isCentre;
RelativeSizeAxes = Axes.Both;
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,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Colour = isCentre ? Color4.White : Color4Extensions.FromHex("#66FFCC"),
Height = minimum_height,
},
};
Colour = isCentre && i == 0 ? Color4.White : colours.ForHitResult(v.Key),
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 float offsetForValue(float value)
{
return availableHeight * value / maxValue;
}
private float heightForValue(float value)
{
return basalHeight + offsetForValue(value);
}
protected override void LoadComplete()
{
base.LoadComplete();
float height = Math.Clamp(value / maxValue, minimum_height, 1);
foreach (var boxOriginal in boxOriginals)
boxOriginal.Height = basalHeight;
if (height > minimum_height)
boxOriginal.ResizeHeightTo(height, duration, Easing.OutQuint);
float offsetValue = 0;
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)
{
bool hasAdjustment = adjustment != value && adjustment / maxValue >= minimum_height;
bool hasAdjustment = adjustment != totalValue;
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);
}
}

View File

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

View File

@ -127,10 +127,10 @@ namespace osu.Game.Screens.Select
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}";
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).

View File

@ -11,9 +11,12 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using osuTK.Input;
namespace osu.Game.Skinning.Editor
{
@ -90,6 +93,47 @@ namespace osu.Game.Skinning.Editor
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 SelectionBlueprint<ISkinnableDrawable> CreateBlueprintFor(ISkinnableDrawable component)

View File

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

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
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, 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>();
[Resolved]
@ -52,9 +54,11 @@ namespace osu.Game.Tests.Visual.Spectator
/// </summary>
/// <param name="userId">The user to start play for.</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;
userModsDictionary[userId] = mods ?? Array.Empty<APIMod>();
userNextFrameDictionary[userId] = 0;
sendPlayingState(userId);
}
@ -73,10 +77,12 @@ namespace osu.Game.Tests.Visual.Spectator
{
BeatmapID = userBeatmapDictionary[userId],
RulesetID = 0,
Mods = userModsDictionary[userId],
State = state
});
userBeatmapDictionary.Remove(userId);
userModsDictionary.Remove(userId);
}
/// <summary>
@ -125,6 +131,7 @@ namespace osu.Game.Tests.Visual.Spectator
// Track the local user's playing beatmap ID.
Debug.Assert(state.BeatmapID != null);
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);
}
@ -158,6 +165,7 @@ namespace osu.Game.Tests.Visual.Spectator
{
BeatmapID = userBeatmapDictionary[userId],
RulesetID = 0,
Mods = userModsDictionary[userId],
State = SpectatedUserState.Playing
});
}

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<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="Sentry" Version="3.20.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" />

View File

@ -61,7 +61,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<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" />
</ItemGroup>
<!-- 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.Core" Version="5.0.14" />
<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="NUnit" Version="3.13.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />