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:
commit
33a435e2ef
@ -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. -->
|
||||
|
43
osu.Game.Rulesets.Catch.Tests/TestSceneCatchTouchInput.cs
Normal file
43
osu.Game.Rulesets.Catch.Tests/TestSceneCatchTouchInput.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
277
osu.Game.Rulesets.Catch/UI/CatchTouchInputMapper.cs
Normal file
277
osu.Game.Rulesets.Catch/UI/CatchTouchInputMapper.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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)})"
|
||||
)
|
||||
)
|
||||
};
|
||||
|
@ -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)})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
BIN
osu.Game.Tests/Resources/Archives/modified-default-20220818.osk
Normal file
BIN
osu.Game.Tests/Resources/Archives/modified-default-20220818.osk
Normal file
Binary file not shown.
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
};
|
||||
});
|
||||
|
@ -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)));
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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]
|
||||
|
@ -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());
|
||||
|
@ -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]
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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", () =>
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -154,7 +154,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
});
|
||||
}
|
||||
|
||||
private int onlineID = 1;
|
||||
private ulong onlineID = 1;
|
||||
|
||||
private APIScoresCollection createScores()
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
108
osu.Game/Graphics/Sprites/SizePreservingSpriteText.cs
Normal file
108
osu.Game/Graphics/Sprites/SizePreservingSpriteText.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ namespace osu.Game.Online
|
||||
{
|
||||
public enum DownloadState
|
||||
{
|
||||
Unknown,
|
||||
NotDownloaded,
|
||||
Downloading,
|
||||
Importing,
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -114,6 +114,7 @@ namespace osu.Game.Online.Rooms
|
||||
|
||||
switch (downloadTracker.State.Value)
|
||||
{
|
||||
case DownloadState.Unknown:
|
||||
case DownloadState.NotDownloaded:
|
||||
availability.Value = BeatmapAvailability.NotDownloaded();
|
||||
break;
|
||||
|
@ -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)),
|
||||
|
@ -839,7 +839,9 @@ namespace osu.Game
|
||||
OnHome = delegate
|
||||
{
|
||||
CloseAllOverlays(false);
|
||||
menuScreen?.MakeCurrent();
|
||||
|
||||
if (menuScreen?.GetChildScreen() != null)
|
||||
menuScreen.MakeCurrent();
|
||||
},
|
||||
}, topMostOverlayContent.Add);
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}));
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
24
osu.Game/Screens/Play/GameplayClockExtensions.cs
Normal file
24
osu.Game/Screens/Play/GameplayClockExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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 =>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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).
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
|
@ -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" />
|
||||
|
@ -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" />
|
||||
|
Loading…
Reference in New Issue
Block a user