1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 19:42:55 +08:00

Merge branch 'master' into remove-nullable-disable-in-the-mods-for-catch-ruleset

This commit is contained in:
andy 2022-07-26 15:37:30 +08:00 committed by GitHub
commit 88db835e76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
98 changed files with 2706 additions and 1284 deletions

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.716.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.722.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.720.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.722.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
private float halfCatcherWidth; private float halfCatcherWidth;
public override int Version => 20220701;
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
{ {

View File

@ -31,6 +31,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private readonly bool isForCurrentRuleset; private readonly bool isForCurrentRuleset;
private readonly double originalOverallDifficulty; private readonly double originalOverallDifficulty;
public override int Version => 20220701;
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
{ {

View File

@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private const double difficulty_multiplier = 0.0675; private const double difficulty_multiplier = 0.0675;
private double hitWindowGreat; private double hitWindowGreat;
public override int Version => 20220701;
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
{ {

View File

@ -23,13 +23,29 @@ namespace osu.Game.Rulesets.Taiko.Tests
protected DrawableTaikoRuleset DrawableRuleset { get; private set; } protected DrawableTaikoRuleset DrawableRuleset { get; private set; }
protected Container PlayfieldContainer { get; private set; } protected Container PlayfieldContainer { get; private set; }
private ControlPointInfo controlPointInfo { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
var controlPointInfo = new ControlPointInfo(); controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint()); controlPointInfo.Add(0, new TimingControlPoint());
IWorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap IWorkingBeatmap beatmap = CreateWorkingBeatmap(CreateBeatmap(new TaikoRuleset().RulesetInfo));
Add(PlayfieldContainer = new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
Height = DEFAULT_PLAYFIELD_CONTAINER_HEIGHT,
Children = new[] { DrawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset)) }
});
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
return new Beatmap
{ {
HitObjects = new List<HitObject> { new Hit { Type = HitType.Centre } }, HitObjects = new List<HitObject> { new Hit { Type = HitType.Centre } },
BeatmapInfo = new BeatmapInfo BeatmapInfo = new BeatmapInfo
@ -41,19 +57,10 @@ namespace osu.Game.Rulesets.Taiko.Tests
Title = @"Sample Beatmap", Title = @"Sample Beatmap",
Author = { Username = @"peppy" }, Author = { Username = @"peppy" },
}, },
Ruleset = new TaikoRuleset().RulesetInfo Ruleset = ruleset
}, },
ControlPointInfo = controlPointInfo ControlPointInfo = controlPointInfo
}); };
Add(PlayfieldContainer = new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
Height = DEFAULT_PLAYFIELD_CONTAINER_HEIGHT,
Children = new[] { DrawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(new TaikoRuleset().RulesetInfo)) }
});
} }
} }
} }

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(200), Size = new Vector2(200),
Child = new InputDrum(playfield.HitObjectContainer) Child = new InputDrum()
} }
}); });
} }

View File

@ -0,0 +1,49 @@
// 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.Taiko.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
{
[TestFixture]
public class TestSceneDrumTouchInputArea : OsuTestScene
{
private DrumTouchInputArea drumTouchInputArea = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create drum", () =>
{
Child = new TaikoInputManager(new TaikoRuleset().RulesetInfo)
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new InputDrum
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Height = 0.2f,
},
drumTouchInputArea = new DrumTouchInputArea
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
},
},
};
});
}
[Test]
public void TestDrum()
{
AddStep("show drum", () => drumTouchInputArea.Show());
}
}
}

View File

@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private const double colour_skill_multiplier = 0.01; private const double colour_skill_multiplier = 0.01;
private const double stamina_skill_multiplier = 0.021; private const double stamina_skill_multiplier = 0.021;
public override int Version => 20220701;
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
{ {

View File

@ -10,8 +10,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
@ -115,9 +113,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public readonly Sprite Rim; public readonly Sprite Rim;
public readonly Sprite Centre; public readonly Sprite Centre;
[Resolved]
private DrumSampleTriggerSource sampleTriggerSource { get; set; }
public LegacyHalfDrum(bool flipped) public LegacyHalfDrum(bool flipped)
{ {
Masking = true; Masking = true;
@ -152,12 +147,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
if (e.Action == CentreAction) if (e.Action == CentreAction)
{ {
target = Centre; target = Centre;
sampleTriggerSource.Play(HitType.Centre);
} }
else if (e.Action == RimAction) else if (e.Action == RimAction)
{ {
target = Rim; target = Rim;
sampleTriggerSource.Play(HitType.Rim);
} }
if (target != null) if (target != null)

View File

@ -4,11 +4,13 @@
#nullable disable #nullable disable
using System.ComponentModel; using System.ComponentModel;
using osu.Framework.Allocation;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko namespace osu.Game.Rulesets.Taiko
{ {
[Cached] // Used for touch input, see DrumTouchInputArea.
public class TaikoInputManager : RulesetInputManager<TaikoAction> public class TaikoInputManager : RulesetInputManager<TaikoAction>
{ {
public TaikoInputManager(RulesetInfo ruleset) public TaikoInputManager(RulesetInfo ruleset)

View File

@ -8,18 +8,18 @@ using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Rulesets.Timing; using osu.Game.Rulesets.Timing;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -56,6 +56,8 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Depth = float.MaxValue Depth = float.MaxValue
}); });
KeyBindingInputManager.Add(new DrumTouchInputArea());
} }
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()

View File

@ -0,0 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko.UI
{
internal class DrumSamplePlayer : CompositeDrawable, IKeyBindingHandler<TaikoAction>
{
private readonly DrumSampleTriggerSource leftRimSampleTriggerSource;
private readonly DrumSampleTriggerSource leftCentreSampleTriggerSource;
private readonly DrumSampleTriggerSource rightCentreSampleTriggerSource;
private readonly DrumSampleTriggerSource rightRimSampleTriggerSource;
public DrumSamplePlayer(HitObjectContainer hitObjectContainer)
{
InternalChildren = new Drawable[]
{
leftRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
leftCentreSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
rightCentreSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
rightRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
};
}
public bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
{
switch (e.Action)
{
case TaikoAction.LeftRim:
leftRimSampleTriggerSource.Play(HitType.Rim);
break;
case TaikoAction.LeftCentre:
leftCentreSampleTriggerSource.Play(HitType.Centre);
break;
case TaikoAction.RightCentre:
rightCentreSampleTriggerSource.Play(HitType.Centre);
break;
case TaikoAction.RightRim:
rightRimSampleTriggerSource.Play(HitType.Rim);
break;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<TaikoAction> e)
{
}
}
}

View File

@ -0,0 +1,243 @@
// 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.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
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.Graphics;
namespace osu.Game.Rulesets.Taiko.UI
{
/// <summary>
/// An overlay that captures and displays osu!taiko mouse and touch input.
/// </summary>
public class DrumTouchInputArea : VisibilityContainer
{
// visibility state affects our child. we always want to handle input.
public override bool PropagatePositionalInputSubTree => true;
public override bool PropagateNonPositionalInputSubTree => true;
private KeyBindingContainer<TaikoAction> keyBindingContainer = null!;
private readonly Dictionary<object, TaikoAction> trackedActions = new Dictionary<object, TaikoAction>();
private Container mainContent = null!;
private QuarterCircle leftCentre = null!;
private QuarterCircle rightCentre = null!;
private QuarterCircle leftRim = null!;
private QuarterCircle rightRim = null!;
[BackgroundDependencyLoader]
private void load(TaikoInputManager taikoInputManager, OsuColour colours)
{
Debug.Assert(taikoInputManager.KeyBindingContainer != null);
keyBindingContainer = taikoInputManager.KeyBindingContainer;
// Container should handle input everywhere.
RelativeSizeAxes = Axes.Both;
const float centre_region = 0.80f;
Children = new Drawable[]
{
new Container
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.X,
Height = 350,
Y = 20,
Masking = true,
FillMode = FillMode.Fit,
Children = new Drawable[]
{
mainContent = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
leftRim = new QuarterCircle(TaikoAction.LeftRim, colours.Blue)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight,
X = -2,
},
rightRim = new QuarterCircle(TaikoAction.RightRim, colours.Blue)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight,
X = 2,
Rotation = 90,
},
leftCentre = new QuarterCircle(TaikoAction.LeftCentre, colours.Pink)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight,
X = -2,
Scale = new Vector2(centre_region),
},
rightCentre = new QuarterCircle(TaikoAction.RightCentre, colours.Pink)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight,
X = 2,
Scale = new Vector2(centre_region),
Rotation = 90,
}
}
},
}
},
};
}
protected override bool OnKeyDown(KeyDownEvent e)
{
// Hide whenever the keyboard is used.
Hide();
return false;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (!validMouse(e))
return false;
handleDown(e.Button, e.ScreenSpaceMousePosition);
return true;
}
protected override void OnMouseUp(MouseUpEvent e)
{
if (!validMouse(e))
return;
handleUp(e.Button);
base.OnMouseUp(e);
}
protected override bool OnTouchDown(TouchDownEvent e)
{
handleDown(e.Touch.Source, e.ScreenSpaceTouchDownPosition);
return true;
}
protected override void OnTouchUp(TouchUpEvent e)
{
handleUp(e.Touch.Source);
base.OnTouchUp(e);
}
private void handleDown(object source, Vector2 position)
{
Show();
TaikoAction taikoAction = getTaikoActionFromInput(position);
// Not too sure how this can happen, but let's avoid throwing.
if (trackedActions.ContainsKey(source))
return;
trackedActions.Add(source, taikoAction);
keyBindingContainer.TriggerPressed(taikoAction);
}
private void handleUp(object source)
{
keyBindingContainer.TriggerReleased(trackedActions[source]);
trackedActions.Remove(source);
}
private bool validMouse(MouseButtonEvent e) =>
leftRim.Contains(e.ScreenSpaceMouseDownPosition) || rightRim.Contains(e.ScreenSpaceMouseDownPosition);
private TaikoAction getTaikoActionFromInput(Vector2 inputPosition)
{
bool centreHit = leftCentre.Contains(inputPosition) || rightCentre.Contains(inputPosition);
bool leftSide = ToLocalSpace(inputPosition).X < DrawWidth / 2;
if (leftSide)
return centreHit ? TaikoAction.LeftCentre : TaikoAction.LeftRim;
return centreHit ? TaikoAction.RightCentre : TaikoAction.RightRim;
}
protected override void PopIn()
{
mainContent.FadeIn(500, Easing.OutQuint);
}
protected override void PopOut()
{
mainContent.FadeOut(300);
}
private class QuarterCircle : CompositeDrawable, IKeyBindingHandler<TaikoAction>
{
private readonly Circle overlay;
private readonly TaikoAction handledAction;
private readonly Circle circle;
public override bool Contains(Vector2 screenSpacePos) => circle.Contains(screenSpacePos);
public QuarterCircle(TaikoAction handledAction, Color4 colour)
{
this.handledAction = handledAction;
RelativeSizeAxes = Axes.Both;
FillMode = FillMode.Fit;
InternalChildren = new Drawable[]
{
new Container
{
Masking = true,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
circle = new Circle
{
RelativeSizeAxes = Axes.Both,
Colour = colour.Multiply(1.4f).Darken(2.8f),
Alpha = 0.8f,
Scale = new Vector2(2),
},
overlay = new Circle
{
Alpha = 0,
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Colour = colour,
Scale = new Vector2(2),
}
}
},
};
}
public bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
{
if (e.Action == handledAction)
overlay.FadeTo(1f, 80, Easing.OutQuint);
return false;
}
public void OnReleased(KeyBindingReleaseEvent<TaikoAction> e)
{
if (e.Action == handledAction)
overlay.FadeOut(1000, Easing.OutQuint);
}
}
}
}

View File

@ -12,8 +12,6 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
@ -27,13 +25,8 @@ namespace osu.Game.Rulesets.Taiko.UI
{ {
private const float middle_split = 0.025f; private const float middle_split = 0.025f;
[Cached] public InputDrum()
private DrumSampleTriggerSource sampleTriggerSource;
public InputDrum(HitObjectContainer hitObjectContainer)
{ {
sampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer);
AutoSizeAxes = Axes.X; AutoSizeAxes = Axes.X;
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
} }
@ -48,7 +41,6 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X, AutoSizeAxes = Axes.X,
}, },
sampleTriggerSource
}; };
} }
@ -116,9 +108,6 @@ namespace osu.Game.Rulesets.Taiko.UI
private readonly Sprite centre; private readonly Sprite centre;
private readonly Sprite centreHit; private readonly Sprite centreHit;
[Resolved]
private DrumSampleTriggerSource sampleTriggerSource { get; set; }
public TaikoHalfDrum(bool flipped) public TaikoHalfDrum(bool flipped)
{ {
Masking = true; Masking = true;
@ -179,15 +168,11 @@ namespace osu.Game.Rulesets.Taiko.UI
{ {
target = centreHit; target = centreHit;
back = centre; back = centre;
sampleTriggerSource.Play(HitType.Centre);
} }
else if (e.Action == RimAction) else if (e.Action == RimAction)
{ {
target = rimHit; target = rimHit;
back = rim; back = rim;
sampleTriggerSource.Play(HitType.Rim);
} }
if (target != null) if (target != null)

View File

@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
inputDrum = new InputDrum(HitObjectContainer) inputDrum = new InputDrum
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
@ -164,6 +164,7 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
drumRollHitContainer.CreateProxy(), drumRollHitContainer.CreateProxy(),
new DrumSamplePlayer(HitObjectContainer),
// this is added at the end of the hierarchy to receive input before taiko objects. // this is added at the end of the hierarchy to receive input before taiko objects.
// but is proxied below everything to not cover visual effects such as hit explosions. // but is proxied below everything to not cover visual effects such as hit explosions.
inputDrum, inputDrum,

View File

@ -0,0 +1,100 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Beatmaps
{
[HeadlessTest]
public class WorkingBeatmapManagerTest : OsuTestScene
{
private BeatmapManager beatmaps = null!;
private BeatmapSetInfo importedSet = null!;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio, RulesetStore rulesets)
{
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("import beatmap", () =>
{
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
});
}
[Test]
public void TestGetWorkingBeatmap() => AddStep("run test", () =>
{
Assert.That(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()), Is.Not.Null);
});
[Test]
public void TestCachedRetrievalNoFiles() => AddStep("run test", () =>
{
var beatmap = importedSet.Beatmaps.First();
Assert.That(beatmap.BeatmapSet?.Files, Is.Empty);
var first = beatmaps.GetWorkingBeatmap(beatmap);
var second = beatmaps.GetWorkingBeatmap(beatmap);
Assert.That(first, Is.SameAs(second));
Assert.That(first.BeatmapInfo.BeatmapSet?.Files, Has.Count.GreaterThan(0));
});
[Test]
public void TestCachedRetrievalWithFiles() => AddStep("run test", () =>
{
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID).Detach());
Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0));
var first = beatmaps.GetWorkingBeatmap(beatmap);
var second = beatmaps.GetWorkingBeatmap(beatmap);
Assert.That(first, Is.SameAs(second));
Assert.That(first.BeatmapInfo.BeatmapSet?.Files, Has.Count.GreaterThan(0));
});
[Test]
public void TestForcedRefetchRetrievalNoFiles() => AddStep("run test", () =>
{
var beatmap = importedSet.Beatmaps.First();
Assert.That(beatmap.BeatmapSet?.Files, Is.Empty);
var first = beatmaps.GetWorkingBeatmap(beatmap);
var second = beatmaps.GetWorkingBeatmap(beatmap, true);
Assert.That(first, Is.Not.SameAs(second));
});
[Test]
public void TestForcedRefetchRetrievalWithFiles() => AddStep("run test", () =>
{
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID).Detach());
Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0));
var first = beatmaps.GetWorkingBeatmap(beatmap);
var second = beatmaps.GetWorkingBeatmap(beatmap, true);
Assert.That(first, Is.Not.SameAs(second));
});
}
}

View File

@ -0,0 +1,132 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Database
{
[HeadlessTest]
public class BackgroundBeatmapProcessorTests : OsuTestScene, ILocalUserPlayInfo
{
public IBindable<bool> IsPlaying => isPlaying;
private readonly Bindable<bool> isPlaying = new Bindable<bool>();
private BeatmapSetInfo importedSet = null!;
[BackgroundDependencyLoader]
private void load(OsuGameBase osu)
{
importedSet = BeatmapImportHelper.LoadQuickOszIntoOsu(osu).GetResultSafely();
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Set not playing", () => isPlaying.Value = false);
}
[Test]
public void TestDifficultyProcessing()
{
AddAssert("Difficulty is initially set", () =>
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
});
});
AddStep("Reset difficulty", () =>
{
Realm.Write(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
foreach (var b in beatmapSetInfo.Beatmaps)
b.StarRating = -1;
});
});
AddStep("Run background processor", () =>
{
Add(new TestBackgroundBeatmapProcessor());
});
AddUntilStep("wait for difficulties repopulated", () =>
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
});
});
}
[Test]
public void TestDifficultyProcessingWhilePlaying()
{
AddAssert("Difficulty is initially set", () =>
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
});
});
AddStep("Set playing", () => isPlaying.Value = true);
AddStep("Reset difficulty", () =>
{
Realm.Write(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
foreach (var b in beatmapSetInfo.Beatmaps)
b.StarRating = -1;
});
});
AddStep("Run background processor", () =>
{
Add(new TestBackgroundBeatmapProcessor());
});
AddWaitStep("wait some", 500);
AddAssert("Difficulty still not populated", () =>
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
return beatmapSetInfo.Beatmaps.All(b => b.StarRating == -1);
});
});
AddStep("Set not playing", () => isPlaying.Value = false);
AddUntilStep("wait for difficulties repopulated", () =>
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
});
});
}
public class TestBackgroundBeatmapProcessor : BackgroundBeatmapProcessor
{
protected override int TimeToSleepDuringGameplay => 10;
}
}
}

View File

@ -142,7 +142,6 @@ namespace osu.Game.Tests.Database
{ {
Task.Run(async () => Task.Run(async () =>
{ {
// ReSharper disable once AccessToDisposedClosure
var beatmapSet = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz")); var beatmapSet = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz"));
Assert.NotNull(beatmapSet); Assert.NotNull(beatmapSet);
@ -311,6 +310,7 @@ namespace osu.Game.Tests.Database
} }
finally finally
{ {
File.Delete(temp);
Directory.Delete(extractedFolder, true); Directory.Delete(extractedFolder, true);
} }
}); });

View File

@ -32,31 +32,29 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestAccessAfterStorageMigrate() public void TestAccessAfterStorageMigrate()
{ {
RunTestWithRealm((realm, storage) => using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target"))
{ {
var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()); RunTestWithRealm((realm, storage) =>
Live<BeatmapInfo>? liveBeatmap = null;
realm.Run(r =>
{ {
r.Write(_ => r.Add(beatmap)); var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata());
liveBeatmap = beatmap.ToLive(realm); Live<BeatmapInfo>? liveBeatmap = null;
});
realm.Run(r =>
{
r.Write(_ => r.Add(beatmap));
liveBeatmap = beatmap.ToLive(realm);
});
using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target"))
{
migratedStorage.DeleteDirectory(string.Empty); migratedStorage.DeleteDirectory(string.Empty);
using (realm.BlockAllOperations("testing")) using (realm.BlockAllOperations("testing"))
{
storage.Migrate(migratedStorage); storage.Migrate(migratedStorage);
}
Assert.IsFalse(liveBeatmap?.PerformRead(l => l.Hidden)); Assert.IsFalse(liveBeatmap?.PerformRead(l => l.Hidden));
} });
}); }
} }
[Test] [Test]
@ -341,14 +339,12 @@ namespace osu.Game.Tests.Database
liveBeatmap.PerformRead(resolved => liveBeatmap.PerformRead(resolved =>
{ {
// retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point. // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
// ReSharper disable once AccessToDisposedClosure
Assert.AreEqual(2, outerRealm.All<BeatmapInfo>().Count()); Assert.AreEqual(2, outerRealm.All<BeatmapInfo>().Count());
Assert.AreEqual(1, changesTriggered); Assert.AreEqual(1, changesTriggered);
// can access properties without a crash. // can access properties without a crash.
Assert.IsFalse(resolved.Hidden); Assert.IsFalse(resolved.Hidden);
// ReSharper disable once AccessToDisposedClosure
outerRealm.Write(r => outerRealm.Write(r =>
{ {
// can use with the main context. // can use with the main context.

View File

@ -4,11 +4,11 @@
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.IO; using osu.Game.IO;
@ -20,22 +20,15 @@ namespace osu.Game.Tests.Database
[TestFixture] [TestFixture]
public abstract class RealmTest public abstract class RealmTest
{ {
private static readonly TemporaryNativeStorage storage; protected void RunTestWithRealm([InstantHandle] Action<RealmAccess, OsuStorage> testAction, [CallerMemberName] string caller = "")
static RealmTest()
{
storage = new TemporaryNativeStorage("realm-test");
storage.DeleteDirectory(string.Empty);
}
protected void RunTestWithRealm(Action<RealmAccess, OsuStorage> testAction, [CallerMemberName] string caller = "")
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(callingMethodName: caller)) using (HeadlessGameHost host = new CleanRunHeadlessGameHost(callingMethodName: caller))
{ {
host.Run(new RealmTestGame(() => host.Run(new RealmTestGame(() =>
{ {
// ReSharper disable once AccessToDisposedClosure var defaultStorage = host.Storage;
var testStorage = new OsuStorage(host, storage.GetStorageForDirectory(caller));
var testStorage = new OsuStorage(host, defaultStorage);
using (var realm = new RealmAccess(testStorage, OsuGameBase.CLIENT_DATABASE_FILENAME)) using (var realm = new RealmAccess(testStorage, OsuGameBase.CLIENT_DATABASE_FILENAME))
{ {
@ -58,7 +51,7 @@ namespace osu.Game.Tests.Database
{ {
host.Run(new RealmTestGame(async () => host.Run(new RealmTestGame(async () =>
{ {
var testStorage = storage.GetStorageForDirectory(caller); var testStorage = host.Storage;
using (var realm = new RealmAccess(testStorage, OsuGameBase.CLIENT_DATABASE_FILENAME)) using (var realm = new RealmAccess(testStorage, OsuGameBase.CLIENT_DATABASE_FILENAME))
{ {
@ -116,7 +109,7 @@ namespace osu.Game.Tests.Database
private class RealmTestGame : Framework.Game private class RealmTestGame : Framework.Game
{ {
public RealmTestGame(Func<Task> work) public RealmTestGame([InstantHandle] Func<Task> work)
{ {
// ReSharper disable once AsyncVoidLambda // ReSharper disable once AsyncVoidLambda
Scheduler.Add(async () => Scheduler.Add(async () =>
@ -126,7 +119,7 @@ namespace osu.Game.Tests.Database
}); });
} }
public RealmTestGame(Action work) public RealmTestGame([InstantHandle] Action work)
{ {
Scheduler.Add(() => Scheduler.Add(() =>
{ {

View File

@ -5,6 +5,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -15,6 +16,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Gameplay namespace osu.Game.Tests.Gameplay
@ -91,6 +93,47 @@ namespace osu.Game.Tests.Gameplay
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0)); Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
} }
[Test]
public void TestFailScore()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects =
{
new TestHitObject(),
new TestHitObject(HitResult.LargeTickHit),
new TestHitObject(HitResult.SmallTickHit),
new TestHitObject(HitResult.SmallBonus),
new TestHitObject(),
new TestHitObject(HitResult.LargeTickHit),
new TestHitObject(HitResult.SmallTickHit),
new TestHitObject(HitResult.LargeBonus),
}
};
var scoreProcessor = new ScoreProcessor(new OsuRuleset());
scoreProcessor.ApplyBeatmap(beatmap);
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].CreateJudgement()) { Type = HitResult.Ok });
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].CreateJudgement()) { Type = HitResult.LargeTickHit });
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], beatmap.HitObjects[2].CreateJudgement()) { Type = HitResult.SmallTickMiss });
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], beatmap.HitObjects[3].CreateJudgement()) { Type = HitResult.SmallBonus });
var score = new ScoreInfo { Ruleset = new OsuRuleset().RulesetInfo };
scoreProcessor.FailScore(score);
Assert.That(score.Rank, Is.EqualTo(ScoreRank.F));
Assert.That(score.Passed, Is.False);
Assert.That(score.Statistics.Count(kvp => kvp.Value > 0), Is.EqualTo(7));
Assert.That(score.Statistics[HitResult.Ok], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.Miss], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.LargeTickHit], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.LargeTickMiss], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.SmallTickMiss], Is.EqualTo(2));
Assert.That(score.Statistics[HitResult.SmallBonus], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.IgnoreMiss], Is.EqualTo(1));
}
private class TestJudgement : Judgement private class TestJudgement : Judgement
{ {
public override HitResult MaxResult { get; } public override HitResult MaxResult { get; }
@ -100,5 +143,17 @@ namespace osu.Game.Tests.Gameplay
MaxResult = maxResult; MaxResult = maxResult;
} }
} }
private class TestHitObject : HitObject
{
private readonly HitResult maxResult;
public TestHitObject(HitResult maxResult = HitResult.Perfect)
{
this.maxResult = maxResult;
}
public override Judgement CreateJudgement() => new TestJudgement(maxResult);
}
} }
} }

View File

@ -12,7 +12,6 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Solo;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -110,30 +109,30 @@ namespace osu.Game.Tests.Online
} }
[Test] [Test]
public void TestDeserialiseSubmittableScoreWithEmptyMods() public void TestDeserialiseSoloScoreWithEmptyMods()
{ {
var score = new SubmittableScore(new ScoreInfo var score = SoloScoreInfo.ForSubmission(new ScoreInfo
{ {
User = new APIUser(), User = new APIUser(),
Ruleset = new OsuRuleset().RulesetInfo, Ruleset = new OsuRuleset().RulesetInfo,
}); });
var deserialised = JsonConvert.DeserializeObject<SubmittableScore>(JsonConvert.SerializeObject(score)); var deserialised = JsonConvert.DeserializeObject<SoloScoreInfo>(JsonConvert.SerializeObject(score));
Assert.That(deserialised?.Mods.Length, Is.Zero); Assert.That(deserialised?.Mods.Length, Is.Zero);
} }
[Test] [Test]
public void TestDeserialiseSubmittableScoreWithCustomModSetting() public void TestDeserialiseSoloScoreWithCustomModSetting()
{ {
var score = new SubmittableScore(new ScoreInfo var score = SoloScoreInfo.ForSubmission(new ScoreInfo
{ {
Mods = new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2 } } }, Mods = new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2 } } },
User = new APIUser(), User = new APIUser(),
Ruleset = new OsuRuleset().RulesetInfo, Ruleset = new OsuRuleset().RulesetInfo,
}); });
var deserialised = JsonConvert.DeserializeObject<SubmittableScore>(JsonConvert.SerializeObject(score)); var deserialised = JsonConvert.DeserializeObject<SoloScoreInfo>(JsonConvert.SerializeObject(score));
Assert.That((deserialised?.Mods[0])?.Settings["speed_change"], Is.EqualTo(2)); Assert.That((deserialised?.Mods[0])?.Settings["speed_change"], Is.EqualTo(2));
} }

View File

@ -6,7 +6,7 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.IO.Serialization; using osu.Game.IO.Serialization;
using osu.Game.Online.Solo; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Online namespace osu.Game.Tests.Online
@ -15,12 +15,12 @@ namespace osu.Game.Tests.Online
/// Basic testing to ensure our attribute-based naming is correctly working. /// Basic testing to ensure our attribute-based naming is correctly working.
/// </summary> /// </summary>
[TestFixture] [TestFixture]
public class TestSubmittableScoreJsonSerialization public class TestSoloScoreInfoJsonSerialization
{ {
[Test] [Test]
public void TestScoreSerialisationViaExtensionMethod() public void TestScoreSerialisationViaExtensionMethod()
{ {
var score = new SubmittableScore(TestResources.CreateTestScoreInfo()); var score = SoloScoreInfo.ForSubmission(TestResources.CreateTestScoreInfo());
string serialised = score.Serialize(); string serialised = score.Serialize();
@ -31,7 +31,7 @@ namespace osu.Game.Tests.Online
[Test] [Test]
public void TestScoreSerialisationWithoutSettings() public void TestScoreSerialisationWithoutSettings()
{ {
var score = new SubmittableScore(TestResources.CreateTestScoreInfo()); var score = SoloScoreInfo.ForSubmission(TestResources.CreateTestScoreInfo());
string serialised = JsonConvert.SerializeObject(score); string serialised = JsonConvert.SerializeObject(score);

View File

@ -5,7 +5,6 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
{ {
@ -14,30 +13,22 @@ namespace osu.Game.Tests.Visual.Editing
public override Drawable CreateTestComponent() => Empty(); public override Drawable CreateTestComponent() => Empty();
[Test] [Test]
[FlakyTest]
/*
* Fail rate around 0.3%
*
* TearDown : osu.Framework.Testing.Drawables.Steps.AssertButton+TracedException : range halved
* --TearDown
* at osu.Framework.Threading.ScheduledDelegate.RunTaskInternal()
* at osu.Framework.Threading.Scheduler.Update()
* at osu.Framework.Graphics.Drawable.UpdateSubTree()
*/
public void TestVisibleRangeUpdatesOnZoomChange() public void TestVisibleRangeUpdatesOnZoomChange()
{ {
double initialVisibleRange = 0; double initialVisibleRange = 0;
AddUntilStep("wait for load", () => MusicController.TrackLoaded);
AddStep("reset zoom", () => TimelineArea.Timeline.Zoom = 100); AddStep("reset zoom", () => TimelineArea.Timeline.Zoom = 100);
AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange); AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange);
AddStep("scale zoom", () => TimelineArea.Timeline.Zoom = 200); AddStep("scale zoom", () => TimelineArea.Timeline.Zoom = 200);
AddAssert("range halved", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange / 2, 1)); AddStep("range halved", () => Assert.That(TimelineArea.Timeline.VisibleRange, Is.EqualTo(initialVisibleRange / 2).Within(1)));
AddStep("descale zoom", () => TimelineArea.Timeline.Zoom = 50); AddStep("descale zoom", () => TimelineArea.Timeline.Zoom = 50);
AddAssert("range doubled", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange * 2, 1)); AddStep("range doubled", () => Assert.That(TimelineArea.Timeline.VisibleRange, Is.EqualTo(initialVisibleRange * 2).Within(1)));
AddStep("restore zoom", () => TimelineArea.Timeline.Zoom = 100); AddStep("restore zoom", () => TimelineArea.Timeline.Zoom = 100);
AddAssert("range restored", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange, 1)); AddStep("range restored", () => Assert.That(TimelineArea.Timeline.VisibleRange, Is.EqualTo(initialVisibleRange).Within(1)));
} }
[Test] [Test]
@ -45,6 +36,8 @@ namespace osu.Game.Tests.Visual.Editing
{ {
double initialVisibleRange = 0; double initialVisibleRange = 0;
AddUntilStep("wait for load", () => MusicController.TrackLoaded);
AddStep("reset timeline size", () => TimelineArea.Timeline.Width = 1); AddStep("reset timeline size", () => TimelineArea.Timeline.Width = 1);
AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange); AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange);

View File

@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Editing
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(30) Colour = OsuColour.Gray(30)
}, },
scrollContainer = new ZoomableScrollContainer scrollContainer = new ZoomableScrollContainer(1, 60, 1)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -80,21 +80,6 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Inner container width matches scroll container", () => innerBox.DrawWidth == scrollContainer.DrawWidth); AddAssert("Inner container width matches scroll container", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
} }
[Test]
public void TestZoomRangeUpdate()
{
AddStep("set zoom to 2", () => scrollContainer.Zoom = 2);
AddStep("set min zoom to 5", () => scrollContainer.MinZoom = 5);
AddAssert("zoom = 5", () => scrollContainer.Zoom == 5);
AddStep("set max zoom to 10", () => scrollContainer.MaxZoom = 10);
AddAssert("zoom = 5", () => scrollContainer.Zoom == 5);
AddStep("set min zoom to 20", () => scrollContainer.MinZoom = 20);
AddStep("set max zoom to 40", () => scrollContainer.MaxZoom = 40);
AddAssert("zoom = 20", () => scrollContainer.Zoom == 20);
}
[Test] [Test]
public void TestZoom0() public void TestZoom0()
{ {

View File

@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public TestScenePause() public TestScenePause()
{ {
base.Content.Add(content = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }); base.Content.Add(content = new GlobalCursorDisplay { RelativeSizeAxes = Axes.Both });
} }
[SetUpSteps] [SetUpSteps]

View File

@ -7,7 +7,6 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Online; using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
@ -15,7 +14,6 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
@ -30,9 +28,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
private const long online_score_id = 2553163309; private const long online_score_id = 2553163309;
[Resolved]
private RulesetStore rulesets { get; set; }
private TestReplayDownloadButton downloadButton; private TestReplayDownloadButton downloadButton;
[Resolved] [Resolved]
@ -211,21 +206,18 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value); AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
} }
private ScoreInfo getScoreInfo(bool replayAvailable, bool hasOnlineId = true) private ScoreInfo getScoreInfo(bool replayAvailable, bool hasOnlineId = true) => new ScoreInfo
{ {
return new APIScore OnlineID = hasOnlineId ? online_score_id : 0,
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(),
Hash = replayAvailable ? "online" : string.Empty,
User = new APIUser
{ {
OnlineID = hasOnlineId ? online_score_id : 0, Id = 39828,
RulesetID = 0, Username = @"WubWoofWolf",
Beatmap = CreateAPIBeatmapSet(new OsuRuleset().RulesetInfo).Beatmaps.First(), }
HasReplay = replayAvailable, };
User = new APIUser
{
Id = 39828,
Username = @"WubWoofWolf",
}
}.CreateScoreInfo(rulesets, beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First());
}
private class TestReplayDownloadButton : ReplayDownloadButton private class TestReplayDownloadButton : ReplayDownloadButton
{ {

View File

@ -3,10 +3,10 @@
#nullable disable #nullable disable
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -40,8 +40,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private TestReplayRecorder recorder; private TestReplayRecorder recorder;
[Cached] private GameplayState gameplayState;
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
@ -52,81 +51,15 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
replay = new Replay(); replay = new Replay();
Add(new GridContainer gameplayState = TestGameplayState.Create(new OsuRuleset());
gameplayState.Score.Replay = replay;
Child = new DependencyProvidingContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Content = new[] CachedDependencies = new (Type, object)[] { (typeof(GameplayState), gameplayState) },
{ Child = createContent(),
new Drawable[] };
{
recordingManager = new TestRulesetInputManager(TestCustomisableModRuleset.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
Recorder = recorder = new TestReplayRecorder(new Score
{
Replay = replay,
ScoreInfo =
{
BeatmapInfo = gameplayState.Beatmap.BeatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
}
})
{
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = Color4.Brown,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Text = "Recording",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestInputConsumer()
}
},
}
},
new Drawable[]
{
playbackManager = new TestRulesetInputManager(TestCustomisableModRuleset.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
ReplayInputHandler = new TestFramedReplayInputHandler(replay)
{
GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos),
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = Color4.DarkBlue,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Text = "Playback",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestInputConsumer()
}
},
}
}
}
});
}); });
} }
@ -203,6 +136,74 @@ namespace osu.Game.Tests.Visual.Gameplay
recorder = null; recorder = null;
} }
private Drawable createContent() => new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
{
recordingManager = new TestRulesetInputManager(TestCustomisableModRuleset.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
Recorder = recorder = new TestReplayRecorder(gameplayState.Score)
{
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = Color4.Brown,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Text = "Recording",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestInputConsumer()
}
},
}
},
new Drawable[]
{
playbackManager = new TestRulesetInputManager(TestCustomisableModRuleset.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
ReplayInputHandler = new TestFramedReplayInputHandler(replay)
{
GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos),
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = Color4.DarkBlue,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Text = "Playback",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestInputConsumer()
}
},
}
}
}
};
public class TestFramedReplayInputHandler : FramedReplayInputHandler<TestReplayFrame> public class TestFramedReplayInputHandler : FramedReplayInputHandler<TestReplayFrame>
{ {
public TestFramedReplayInputHandler(Replay replay) public TestFramedReplayInputHandler(Replay replay)

View File

@ -5,6 +5,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
@ -43,6 +44,21 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("spectator client sent correct ruleset", () => spectatorClient.WatchedUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID); AddAssert("spectator client sent correct ruleset", () => spectatorClient.WatchedUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID);
} }
[Test]
public void TestRestart()
{
AddAssert("spectator client sees playing state", () => spectatorClient.WatchedUserStates[dummy_user_id].State == SpectatedUserState.Playing);
AddStep("exit player", () => Player.Exit());
AddStep("reload player", LoadPlayer);
AddUntilStep("wait for player load", () => Player.IsLoaded && Player.Alpha == 1);
AddAssert("spectator client sees playing state", () => spectatorClient.WatchedUserStates[dummy_user_id].State == SpectatedUserState.Playing);
AddWaitStep("wait", 5);
AddUntilStep("spectator client still sees playing state", () => spectatorClient.WatchedUserStates[dummy_user_id].State == SpectatedUserState.Playing);
}
public override void TearDownSteps() public override void TearDownSteps()
{ {
base.TearDownSteps(); base.TearDownSteps();

View File

@ -24,7 +24,6 @@ using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -633,7 +632,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("invoke on back button", () => multiplayerComponents.OnBackButton()); AddStep("invoke on back button", () => multiplayerComponents.OnBackButton());
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<UserModSelectOverlay>().Single().State.Value == Visibility.Hidden); AddAssert("mod overlay is hidden", () => this.ChildrenOfType<RoomSubScreen>().Single().UserModsSelectOverlay.State.Value == Visibility.Hidden);
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden); AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);

View File

@ -16,6 +16,10 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet.Scores; using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Select.Details;
using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
@ -34,6 +38,9 @@ namespace osu.Game.Tests.Visual.Online
[Resolved] [Resolved]
private IRulesetStore rulesets { get; set; } private IRulesetStore rulesets { get; set; }
[SetUp]
public void SetUp() => Schedule(() => SelectedMods.Value = Array.Empty<Mod>());
[Test] [Test]
public void TestLoading() public void TestLoading()
{ {
@ -205,6 +212,21 @@ namespace osu.Game.Tests.Visual.Online
}); });
} }
[Test]
public void TestSelectedModsDontAffectStatistics()
{
AddStep("show map", () => overlay.ShowBeatmapSet(getBeatmapSet()));
AddAssert("AR displayed as 0", () => overlay.ChildrenOfType<AdvancedStats.StatisticRow>().Single(s => s.Title == BeatmapsetsStrings.ShowStatsAr).Value == (0, null));
AddStep("set AR10 diff adjust", () => SelectedMods.Value = new[]
{
new OsuModDifficultyAdjust
{
ApproachRate = { Value = 10 }
}
});
AddAssert("AR still displayed as 0", () => overlay.ChildrenOfType<AdvancedStats.StatisticRow>().Single(s => s.Title == BeatmapsetsStrings.ShowStatsAr).Value == (0, null));
}
[Test] [Test]
public void TestHide() public void TestHide()
{ {

View File

@ -141,6 +141,19 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("best score not displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 1); AddUntilStep("best score not displayed", () => scoresContainer.ChildrenOfType<DrawableTopScore>().Count() == 1);
} }
[Test]
public void TestUnprocessedPP()
{
AddStep("Load scores with unprocessed PP", () =>
{
var allScores = createScores();
allScores.Scores[0].PP = null;
allScores.UserScore = createUserBest();
allScores.UserScore.Score.PP = null;
scoresContainer.Scores = allScores;
});
}
private int onlineID = 1; private int onlineID = 1;
private APIScoresCollection createScores() private APIScoresCollection createScores()

View File

@ -7,6 +7,7 @@ using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -99,6 +100,23 @@ namespace osu.Game.Tests.Visual.Online
Accuracy = 0.55879 Accuracy = 0.55879
}; };
var unprocessedPPScore = new SoloScoreInfo
{
Rank = ScoreRank.B,
Beatmap = new APIBeatmap
{
BeatmapSet = new APIBeatmapSet
{
Title = "C18H27NO3(extend)",
Artist = "Team Grimoire",
},
DifficultyName = "[4K] Cataclysmic Hypernova",
Status = BeatmapOnlineStatus.Ranked,
},
EndedAt = DateTimeOffset.Now,
Accuracy = 0.55879
};
Add(new FillFlowContainer Add(new FillFlowContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -112,6 +130,7 @@ namespace osu.Game.Tests.Visual.Online
new ColourProvidedContainer(OverlayColourScheme.Green, new DrawableProfileScore(firstScore)), new ColourProvidedContainer(OverlayColourScheme.Green, new DrawableProfileScore(firstScore)),
new ColourProvidedContainer(OverlayColourScheme.Green, new DrawableProfileScore(secondScore)), new ColourProvidedContainer(OverlayColourScheme.Green, new DrawableProfileScore(secondScore)),
new ColourProvidedContainer(OverlayColourScheme.Pink, new DrawableProfileScore(noPPScore)), new ColourProvidedContainer(OverlayColourScheme.Pink, new DrawableProfileScore(noPPScore)),
new ColourProvidedContainer(OverlayColourScheme.Pink, new DrawableProfileScore(unprocessedPPScore)),
new ColourProvidedContainer(OverlayColourScheme.Pink, new DrawableProfileWeightedScore(firstScore, 0.97)), new ColourProvidedContainer(OverlayColourScheme.Pink, new DrawableProfileWeightedScore(firstScore, 0.97)),
new ColourProvidedContainer(OverlayColourScheme.Pink, new DrawableProfileWeightedScore(secondScore, 0.85)), new ColourProvidedContainer(OverlayColourScheme.Pink, new DrawableProfileWeightedScore(secondScore, 0.85)),
new ColourProvidedContainer(OverlayColourScheme.Pink, new DrawableProfileWeightedScore(thirdScore, 0.66)), new ColourProvidedContainer(OverlayColourScheme.Pink, new DrawableProfileWeightedScore(thirdScore, 0.66)),

View File

@ -21,12 +21,12 @@ namespace osu.Game.Tests.Visual.UserInterface
[TestFixture] [TestFixture]
public class TestSceneCursors : OsuManualInputManagerTestScene public class TestSceneCursors : OsuManualInputManagerTestScene
{ {
private readonly MenuCursorContainer menuCursorContainer; private readonly GlobalCursorDisplay globalCursorDisplay;
private readonly CustomCursorBox[] cursorBoxes = new CustomCursorBox[6]; private readonly CustomCursorBox[] cursorBoxes = new CustomCursorBox[6];
public TestSceneCursors() public TestSceneCursors()
{ {
Child = menuCursorContainer = new MenuCursorContainer Child = globalCursorDisplay = new GlobalCursorDisplay
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new[] Children = new[]
@ -96,11 +96,11 @@ namespace osu.Game.Tests.Visual.UserInterface
private void testUserCursor() private void testUserCursor()
{ {
AddStep("Move to green area", () => InputManager.MoveMouseTo(cursorBoxes[0])); AddStep("Move to green area", () => InputManager.MoveMouseTo(cursorBoxes[0]));
AddAssert("Check green cursor visible", () => checkVisible(cursorBoxes[0].Cursor)); AddAssert("Check green cursor visible", () => checkVisible(cursorBoxes[0].MenuCursor));
AddAssert("Check green cursor at mouse", () => checkAtMouse(cursorBoxes[0].Cursor)); AddAssert("Check green cursor at mouse", () => checkAtMouse(cursorBoxes[0].MenuCursor));
AddStep("Move out", moveOut); AddStep("Move out", moveOut);
AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].Cursor)); AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].MenuCursor));
AddAssert("Check global cursor visible", () => checkVisible(menuCursorContainer.Cursor)); AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor));
} }
/// <summary> /// <summary>
@ -111,13 +111,13 @@ namespace osu.Game.Tests.Visual.UserInterface
private void testLocalCursor() private void testLocalCursor()
{ {
AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3])); AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3]));
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor)); AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor));
AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor)); AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].MenuCursor));
AddAssert("Check global cursor visible", () => checkVisible(menuCursorContainer.Cursor)); AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor));
AddAssert("Check global cursor at mouse", () => checkAtMouse(menuCursorContainer.Cursor)); AddAssert("Check global cursor at mouse", () => checkAtMouse(globalCursorDisplay.MenuCursor));
AddStep("Move out", moveOut); AddStep("Move out", moveOut);
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor)); AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor));
AddAssert("Check global cursor visible", () => checkVisible(menuCursorContainer.Cursor)); AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor));
} }
/// <summary> /// <summary>
@ -128,12 +128,12 @@ namespace osu.Game.Tests.Visual.UserInterface
private void testUserCursorOverride() private void testUserCursorOverride()
{ {
AddStep("Move to blue-green boundary", () => InputManager.MoveMouseTo(cursorBoxes[1].ScreenSpaceDrawQuad.BottomRight - new Vector2(10))); AddStep("Move to blue-green boundary", () => InputManager.MoveMouseTo(cursorBoxes[1].ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].Cursor)); AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].MenuCursor));
AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].Cursor)); AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].MenuCursor));
AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].Cursor)); AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].MenuCursor));
AddStep("Move out", moveOut); AddStep("Move out", moveOut);
AddAssert("Check blue cursor not visible", () => !checkVisible(cursorBoxes[1].Cursor)); AddAssert("Check blue cursor not visible", () => !checkVisible(cursorBoxes[1].MenuCursor));
AddAssert("Check green cursor not visible", () => !checkVisible(cursorBoxes[0].Cursor)); AddAssert("Check green cursor not visible", () => !checkVisible(cursorBoxes[0].MenuCursor));
} }
/// <summary> /// <summary>
@ -143,13 +143,13 @@ namespace osu.Game.Tests.Visual.UserInterface
private void testMultipleLocalCursors() private void testMultipleLocalCursors()
{ {
AddStep("Move to yellow-purple boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.BottomRight - new Vector2(10))); AddStep("Move to yellow-purple boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor)); AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor));
AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor)); AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].MenuCursor));
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor)); AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].Cursor)); AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].MenuCursor));
AddStep("Move out", moveOut); AddStep("Move out", moveOut);
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor)); AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor));
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor)); AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
} }
/// <summary> /// <summary>
@ -159,13 +159,13 @@ namespace osu.Game.Tests.Visual.UserInterface
private void testUserOverrideWithLocal() private void testUserOverrideWithLocal()
{ {
AddStep("Move to yellow-blue boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.TopRight - new Vector2(10))); AddStep("Move to yellow-blue boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.TopRight - new Vector2(10)));
AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].Cursor)); AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].MenuCursor));
AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].Cursor)); AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].MenuCursor));
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor)); AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].Cursor)); AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].MenuCursor));
AddStep("Move out", moveOut); AddStep("Move out", moveOut);
AddAssert("Check blue cursor invisible", () => !checkVisible(cursorBoxes[1].Cursor)); AddAssert("Check blue cursor invisible", () => !checkVisible(cursorBoxes[1].MenuCursor));
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor)); AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
} }
/// <summary> /// <summary>
@ -191,7 +191,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
public bool SmoothTransition; public bool SmoothTransition;
public CursorContainer Cursor { get; } public CursorContainer MenuCursor { get; }
public bool ProvidingUserCursor { get; } public bool ProvidingUserCursor { get; }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || (SmoothTransition && !ProvidingUserCursor); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || (SmoothTransition && !ProvidingUserCursor);
@ -218,7 +218,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Origin = Anchor.Centre, Origin = Anchor.Centre,
Text = providesUserCursor ? "User cursor" : "Local cursor" Text = providesUserCursor ? "User cursor" : "Local cursor"
}, },
Cursor = new TestCursorContainer MenuCursor = new TestCursorContainer
{ {
State = { Value = providesUserCursor ? Visibility.Hidden : Visibility.Visible }, State = { Value = providesUserCursor ? Visibility.Hidden : Visibility.Visible },
} }

View File

@ -8,8 +8,10 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -17,11 +19,26 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
public class TestSceneLabelledSliderBar : OsuTestScene public class TestSceneLabelledSliderBar : OsuTestScene
{ {
[TestCase(false)] [Test]
[TestCase(true)] public void TestBasic() => createSliderBar();
public void TestSliderBar(bool hasDescription) => createSliderBar(hasDescription);
private void createSliderBar(bool hasDescription = false) [Test]
public void TestDescription()
{
createSliderBar();
AddStep("set description", () => this.ChildrenOfType<LabelledSliderBar<double>>().ForEach(l => l.Description = "this text describes the component"));
}
[Test]
public void TestSize()
{
createSliderBar();
AddStep("set zero width", () => this.ChildrenOfType<LabelledSliderBar<double>>().ForEach(l => l.ResizeWidthTo(0, 200, Easing.OutQuint)));
AddStep("set negative width", () => this.ChildrenOfType<LabelledSliderBar<double>>().ForEach(l => l.ResizeWidthTo(-1, 200, Easing.OutQuint)));
AddStep("revert back", () => this.ChildrenOfType<LabelledSliderBar<double>>().ForEach(l => l.ResizeWidthTo(1, 200, Easing.OutQuint)));
}
private void createSliderBar()
{ {
AddStep("create component", () => AddStep("create component", () =>
{ {
@ -38,6 +55,8 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
new LabelledSliderBar<double> new LabelledSliderBar<double>
{ {
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Current = new BindableDouble(5) Current = new BindableDouble(5)
{ {
MinValue = 0, MinValue = 0,
@ -45,7 +64,6 @@ namespace osu.Game.Tests.Visual.UserInterface
Precision = 1, Precision = 1,
}, },
Label = "a sample component", Label = "a sample component",
Description = hasDescription ? "this text describes the component" : string.Empty,
}, },
}, },
}; };
@ -54,10 +72,14 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
flow.Add(new OverlayColourContainer(colour) flow.Add(new OverlayColourContainer(colour)
{ {
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Child = new LabelledSliderBar<double> Child = new LabelledSliderBar<double>
{ {
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Current = new BindableDouble(5) Current = new BindableDouble(5)
{ {
MinValue = 0, MinValue = 0,
@ -65,7 +87,6 @@ namespace osu.Game.Tests.Visual.UserInterface
Precision = 1, Precision = 1,
}, },
Label = "a sample component", Label = "a sample component",
Description = hasDescription ? "this text describes the component" : string.Empty,
} }
}); });
} }

View File

@ -0,0 +1,77 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneModPresetColumn : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Test]
public void TestBasicAppearance()
{
ModPresetColumn modPresetColumn = null!;
AddStep("create content", () => Child = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(30),
Child = modPresetColumn = new ModPresetColumn
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Presets = createTestPresets().ToArray()
}
});
AddStep("change presets", () => modPresetColumn.Presets = createTestPresets().Skip(1).ToArray());
}
private static IEnumerable<ModPreset> createTestPresets() => new[]
{
new ModPreset
{
Name = "First preset",
Description = "Please ignore",
Mods = new Mod[]
{
new OsuModHardRock(),
new OsuModDoubleTime()
}
},
new ModPreset
{
Name = "AR0",
Description = "For good readers",
Mods = new Mod[]
{
new OsuModDifficultyAdjust
{
ApproachRate = { Value = 0 }
}
}
},
new ModPreset
{
Name = "This preset is going to have an extraordinarily long name",
Description = "This is done so that the capability to truncate overlong texts may be demonstrated",
Mods = new Mod[]
{
new OsuModFlashlight(),
new OsuModSpinIn()
}
}
};
}
}

View File

@ -0,0 +1,74 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public class TestSceneModPresetPanel : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Test]
public void TestVariousModPresets()
{
AddStep("create content", () => Child = new FillFlowContainer
{
Width = 300,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spacing = new Vector2(0, 5),
ChildrenEnumerable = createTestPresets().Select(preset => new ModPresetPanel(preset))
});
}
private static IEnumerable<ModPreset> createTestPresets() => new[]
{
new ModPreset
{
Name = "First preset",
Description = "Please ignore",
Mods = new Mod[]
{
new OsuModHardRock(),
new OsuModDoubleTime()
}
},
new ModPreset
{
Name = "AR0",
Description = "For good readers",
Mods = new Mod[]
{
new OsuModDifficultyAdjust
{
ApproachRate = { Value = 0 }
}
}
},
new ModPreset
{
Name = "This preset is going to have an extraordinarily long name",
Description = "This is done so that the capability to truncate overlong texts may be demonstrated",
Mods = new Mod[]
{
new OsuModFlashlight(),
new OsuModSpinIn()
}
}
};
}
}

View File

@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Spacing = new Vector2(2f), Spacing = new Vector2(2f),
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
ChildrenEnumerable = Enumerable.Range(0, 15).Select(i => new FillFlowContainer ChildrenEnumerable = Enumerable.Range(-1, 15).Select(i => new FillFlowContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -6,6 +6,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -27,7 +28,6 @@ namespace osu.Game.Tournament.Tests.NonVisual
{ {
var osu = new TestTournament(runOnLoadComplete: () => var osu = new TestTournament(runOnLoadComplete: () =>
{ {
// ReSharper disable once AccessToDisposedClosure
var storage = host.Storage.GetStorageForDirectory(Path.Combine("tournaments", "default")); var storage = host.Storage.GetStorageForDirectory(Path.Combine("tournaments", "default"));
using (var stream = storage.CreateFileSafely("bracket.json")) using (var stream = storage.CreateFileSafely("bracket.json"))
@ -85,7 +85,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
public new Task BracketLoadTask => base.BracketLoadTask; public new Task BracketLoadTask => base.BracketLoadTask;
public TestTournament(bool resetRuleset = false, Action runOnLoadComplete = null) public TestTournament(bool resetRuleset = false, [InstantHandle] Action runOnLoadComplete = null)
{ {
this.resetRuleset = resetRuleset; this.resetRuleset = resetRuleset;
this.runOnLoadComplete = runOnLoadComplete; this.runOnLoadComplete = runOnLoadComplete;

View File

@ -70,10 +70,10 @@ namespace osu.Game.Tournament
protected override void LoadComplete() protected override void LoadComplete()
{ {
MenuCursorContainer.Cursor.AlwaysPresent = true; // required for tooltip display GlobalCursorDisplay.MenuCursor.AlwaysPresent = true; // required for tooltip display
// we don't want to show the menu cursor as it would appear on stream output. // we don't want to show the menu cursor as it would appear on stream output.
MenuCursorContainer.Cursor.Alpha = 0; GlobalCursorDisplay.MenuCursor.Alpha = 0;
base.LoadComplete(); base.LoadComplete();

View File

@ -0,0 +1,144 @@
// 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.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets;
using osu.Game.Screens.Play;
namespace osu.Game
{
public class BackgroundBeatmapProcessor : Component
{
[Resolved]
private RulesetStore rulesetStore { get; set; } = null!;
[Resolved]
private RealmAccess realmAccess { get; set; } = null!;
[Resolved]
private BeatmapUpdater beatmapUpdater { get; set; } = null!;
[Resolved]
private IBindable<WorkingBeatmap> gameBeatmap { get; set; } = null!;
[Resolved]
private ILocalUserPlayInfo? localUserPlayInfo { get; set; }
protected virtual int TimeToSleepDuringGameplay => 30000;
protected override void LoadComplete()
{
base.LoadComplete();
Task.Run(() =>
{
Logger.Log("Beginning background beatmap processing..");
checkForOutdatedStarRatings();
processBeatmapSetsWithMissingMetrics();
}).ContinueWith(t =>
{
if (t.Exception?.InnerException is ObjectDisposedException)
{
Logger.Log("Finished background aborted during shutdown");
return;
}
Logger.Log("Finished background beatmap processing!");
});
}
/// <summary>
/// Check whether the databased difficulty calculation version matches the latest ruleset provided version.
/// If it doesn't, clear out any existing difficulties so they can be incrementally recalculated.
/// </summary>
private void checkForOutdatedStarRatings()
{
foreach (var ruleset in rulesetStore.AvailableRulesets)
{
// beatmap being passed in is arbitrary here. just needs to be non-null.
int currentVersion = ruleset.CreateInstance().CreateDifficultyCalculator(gameBeatmap.Value).Version;
if (ruleset.LastAppliedDifficultyVersion < currentVersion)
{
Logger.Log($"Resetting star ratings for {ruleset.Name} (difficulty calculation version updated from {ruleset.LastAppliedDifficultyVersion} to {currentVersion})");
int countReset = 0;
realmAccess.Write(r =>
{
foreach (var b in r.All<BeatmapInfo>())
{
if (b.Ruleset.ShortName == ruleset.ShortName)
{
b.StarRating = -1;
countReset++;
}
}
r.Find<RulesetInfo>(ruleset.ShortName).LastAppliedDifficultyVersion = currentVersion;
});
Logger.Log($"Finished resetting {countReset} beatmap sets for {ruleset.Name}");
}
}
}
private void processBeatmapSetsWithMissingMetrics()
{
HashSet<Guid> beatmapSetIds = new HashSet<Guid>();
Logger.Log("Querying for beatmap sets to reprocess...");
realmAccess.Run(r =>
{
foreach (var b in r.All<BeatmapInfo>().Where(b => b.StarRating < 0 || (b.OnlineID > 0 && b.LastOnlineUpdate == null)))
{
Debug.Assert(b.BeatmapSet != null);
beatmapSetIds.Add(b.BeatmapSet.ID);
}
});
Logger.Log($"Found {beatmapSetIds.Count} beatmap sets which require reprocessing.");
int i = 0;
foreach (var id in beatmapSetIds)
{
while (localUserPlayInfo?.IsPlaying.Value == true)
{
Logger.Log("Background processing sleeping due to active gameplay...");
Thread.Sleep(TimeToSleepDuringGameplay);
}
realmAccess.Run(r =>
{
var set = r.Find<BeatmapSetInfo>(id);
if (set != null)
{
try
{
Logger.Log($"Background processing {set} ({++i} / {beatmapSetIds.Count})");
beatmapUpdater.Process(set);
}
catch (Exception e)
{
Logger.Log($"Background processing failed on {set}: {e}");
}
}
});
}
}
}
}

View File

@ -87,7 +87,11 @@ namespace osu.Game.Beatmaps
public string Hash { get; set; } = string.Empty; public string Hash { get; set; } = string.Empty;
public double StarRating { get; set; } /// <summary>
/// Defaults to -1 (meaning not-yet-calculated).
/// Will likely be superseded with a better storage considering ruleset/mods.
/// </summary>
public double StarRating { get; set; } = -1;
[Indexed] [Indexed]
public string MD5Hash { get; set; } = string.Empty; public string MD5Hash { get; set; } = string.Empty;

View File

@ -439,12 +439,15 @@ namespace osu.Game.Beatmaps
{ {
if (beatmapInfo != null) if (beatmapInfo != null)
{ {
// Detached sets don't come with files. if (refetch)
// If we seem to be missing files, now is a good time to re-fetch.
if (refetch || beatmapInfo.IsManaged || beatmapInfo.BeatmapSet?.Files.Count == 0)
{
workingBeatmapCache.Invalidate(beatmapInfo); workingBeatmapCache.Invalidate(beatmapInfo);
// Detached beatmapsets don't come with files as an optimisation (see `RealmObjectExtensions.beatmap_set_mapper`).
// If we seem to be missing files, now is a good time to re-fetch.
bool missingFiles = beatmapInfo.BeatmapSet?.Files.Count == 0;
if (refetch || beatmapInfo.IsManaged || missingFiles)
{
Guid id = beatmapInfo.ID; Guid id = beatmapInfo.ID;
beatmapInfo = Realm.Run(r => r.Find<BeatmapInfo>(id)?.Detach()) ?? beatmapInfo; beatmapInfo = Realm.Run(r => r.Find<BeatmapInfo>(id)?.Detach()) ?? beatmapInfo;
} }

View File

@ -151,7 +151,7 @@ namespace osu.Game.Beatmaps.Drawables
displayedStars.BindValueChanged(s => displayedStars.BindValueChanged(s =>
{ {
starsText.Text = s.NewValue.ToLocalisableString("0.00"); starsText.Text = s.NewValue < 0 ? "-" : s.NewValue.ToLocalisableString("0.00");
background.Colour = colours.ForStarDifficulty(s.NewValue); background.Colour = colours.ForStarDifficulty(s.NewValue);

View File

@ -91,6 +91,7 @@ namespace osu.Game.Configuration
// Input // Input
SetDefault(OsuSetting.MenuCursorSize, 1.0f, 0.5f, 2f, 0.01f); SetDefault(OsuSetting.MenuCursorSize, 1.0f, 0.5f, 2f, 0.01f);
SetDefault(OsuSetting.GameplayCursorSize, 1.0f, 0.1f, 2f, 0.01f); SetDefault(OsuSetting.GameplayCursorSize, 1.0f, 0.1f, 2f, 0.01f);
SetDefault(OsuSetting.GameplayCursorDuringTouch, false);
SetDefault(OsuSetting.AutoCursorSize, false); SetDefault(OsuSetting.AutoCursorSize, false);
SetDefault(OsuSetting.MouseDisableButtons, false); SetDefault(OsuSetting.MouseDisableButtons, false);
@ -292,6 +293,7 @@ namespace osu.Game.Configuration
MenuCursorSize, MenuCursorSize,
GameplayCursorSize, GameplayCursorSize,
AutoCursorSize, AutoCursorSize,
GameplayCursorDuringTouch,
DimLevel, DimLevel,
BlurLevel, BlurLevel,
LightenDuringBreaks, LightenDuringBreaks,

View File

@ -63,8 +63,9 @@ namespace osu.Game.Database
/// 17 2022-07-16 Added CountryCode to RealmUser. /// 17 2022-07-16 Added CountryCode to RealmUser.
/// 18 2022-07-19 Added OnlineMD5Hash and LastOnlineUpdate to BeatmapInfo. /// 18 2022-07-19 Added OnlineMD5Hash and LastOnlineUpdate to BeatmapInfo.
/// 19 2022-07-19 Added DateSubmitted and DateRanked to BeatmapSetInfo. /// 19 2022-07-19 Added DateSubmitted and DateRanked to BeatmapSetInfo.
/// 20 2022-07-21 Added LastAppliedDifficultyVersion to RulesetInfo, changed default value of BeatmapInfo.StarRating to -1.
/// </summary> /// </summary>
private const int schema_version = 19; private const int schema_version = 20;
/// <summary> /// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods. /// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@ -780,6 +781,15 @@ namespace osu.Game.Database
case 14: case 14:
foreach (var beatmap in migration.NewRealm.All<BeatmapInfo>()) foreach (var beatmap in migration.NewRealm.All<BeatmapInfo>())
beatmap.UserSettings = new BeatmapUserSettings(); beatmap.UserSettings = new BeatmapUserSettings();
break;
case 20:
// As we now have versioned difficulty calculations, let's reset
// all star ratings and have `BackgroundBeatmapProcessor` recalculate them.
foreach (var beatmap in migration.NewRealm.All<BeatmapInfo>())
beatmap.StarRating = -1;
break; break;
} }
} }

View File

@ -0,0 +1,92 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input;
using osu.Framework.Input.StateChanges;
using osu.Game.Configuration;
namespace osu.Game.Graphics.Cursor
{
/// <summary>
/// A container which provides the main <see cref="Cursor.MenuCursor"/>.
/// Also handles cases where a more localised cursor is provided by another component (via <see cref="IProvideCursor"/>).
/// </summary>
public class GlobalCursorDisplay : Container, IProvideCursor
{
/// <summary>
/// Control whether any cursor should be displayed.
/// </summary>
internal bool ShowCursor = true;
public CursorContainer MenuCursor { get; }
public bool ProvidingUserCursor => true;
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
private Bindable<bool> showDuringTouch = null!;
private InputManager inputManager = null!;
private IProvideCursor? currentOverrideProvider;
[Resolved]
private OsuConfigManager config { get; set; } = null!;
public GlobalCursorDisplay()
{
AddRangeInternal(new Drawable[]
{
MenuCursor = new MenuCursor { State = { Value = Visibility.Hidden } },
Content = new Container { RelativeSizeAxes = Axes.Both }
});
}
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
showDuringTouch = config.GetBindable<bool>(OsuSetting.GameplayCursorDuringTouch);
}
protected override void Update()
{
base.Update();
var lastMouseSource = inputManager.CurrentState.Mouse.LastSource;
bool hasValidInput = lastMouseSource != null && (showDuringTouch.Value || lastMouseSource is not ISourcedFromTouch);
if (!hasValidInput || !ShowCursor)
{
currentOverrideProvider?.MenuCursor?.Hide();
currentOverrideProvider = null;
return;
}
IProvideCursor newOverrideProvider = this;
foreach (var d in inputManager.HoveredDrawables)
{
if (d is IProvideCursor p && p.ProvidingUserCursor)
{
newOverrideProvider = p;
break;
}
}
if (currentOverrideProvider == newOverrideProvider)
return;
currentOverrideProvider?.MenuCursor?.Hide();
newOverrideProvider.MenuCursor?.Show();
currentOverrideProvider = newOverrideProvider;
}
}
}

View File

@ -17,10 +17,10 @@ namespace osu.Game.Graphics.Cursor
/// The cursor provided by this <see cref="IDrawable"/>. /// The cursor provided by this <see cref="IDrawable"/>.
/// May be null if no cursor should be visible. /// May be null if no cursor should be visible.
/// </summary> /// </summary>
CursorContainer Cursor { get; } CursorContainer MenuCursor { get; }
/// <summary> /// <summary>
/// Whether <see cref="Cursor"/> should be displayed as the singular user cursor. This will temporarily hide any other user cursor. /// Whether <see cref="MenuCursor"/> should be displayed as the singular user cursor. This will temporarily hide any other user cursor.
/// This value is checked every frame and may be used to control whether multiple cursors are displayed (e.g. watching replays). /// This value is checked every frame and may be used to control whether multiple cursors are displayed (e.g. watching replays).
/// </summary> /// </summary>
bool ProvidingUserCursor { get; } bool ProvidingUserCursor { get; }

View File

@ -1,83 +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.
#nullable disable
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input;
using osu.Framework.Input.StateChanges;
namespace osu.Game.Graphics.Cursor
{
/// <summary>
/// A container which provides a <see cref="MenuCursor"/> which can be overridden by hovered <see cref="Drawable"/>s.
/// </summary>
public class MenuCursorContainer : Container, IProvideCursor
{
protected override Container<Drawable> Content => content;
private readonly Container content;
/// <summary>
/// Whether any cursors can be displayed.
/// </summary>
internal bool CanShowCursor = true;
public CursorContainer Cursor { get; }
public bool ProvidingUserCursor => true;
public MenuCursorContainer()
{
AddRangeInternal(new Drawable[]
{
Cursor = new MenuCursor { State = { Value = Visibility.Hidden } },
content = new Container { RelativeSizeAxes = Axes.Both }
});
}
private InputManager inputManager;
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
}
private IProvideCursor currentTarget;
protected override void Update()
{
base.Update();
var lastMouseSource = inputManager.CurrentState.Mouse.LastSource;
bool hasValidInput = lastMouseSource != null && !(lastMouseSource is ISourcedFromTouch);
if (!hasValidInput || !CanShowCursor)
{
currentTarget?.Cursor?.Hide();
currentTarget = null;
return;
}
IProvideCursor newTarget = this;
foreach (var d in inputManager.HoveredDrawables)
{
if (d is IProvideCursor p && p.ProvidingUserCursor)
{
newTarget = p;
break;
}
}
if (currentTarget == newTarget)
return;
currentTarget?.Cursor?.Hide();
newTarget.Cursor?.Show();
currentTarget = newTarget;
}
}
}

View File

@ -10,9 +10,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Threading; using osu.Framework.Timing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -22,8 +21,8 @@ namespace osu.Game.Graphics.UserInterface
{ {
public class FPSCounter : VisibilityContainer, IHasCustomTooltip public class FPSCounter : VisibilityContainer, IHasCustomTooltip
{ {
private RollingCounter<double> counterUpdateFrameTime = null!; private OsuSpriteText counterUpdateFrameTime = null!;
private RollingCounter<double> counterDrawFPS = null!; private OsuSpriteText counterDrawFPS = null!;
private Container mainContent = null!; private Container mainContent = null!;
@ -31,10 +30,32 @@ namespace osu.Game.Graphics.UserInterface
private Container counters = null!; private Container counters = null!;
private const double min_time_between_updates = 10;
private const double spike_time_ms = 20;
private const float idle_background_alpha = 0.4f; private const float idle_background_alpha = 0.4f;
private readonly BindableBool showFpsDisplay = new BindableBool(true); private readonly BindableBool showFpsDisplay = new BindableBool(true);
private double displayedFpsCount;
private double displayedFrameTime;
private bool isDisplayed;
private double aimDrawFPS;
private double aimUpdateFPS;
private double lastUpdate;
private ThrottledFrameClock drawClock = null!;
private ThrottledFrameClock updateClock = null!;
private ThrottledFrameClock inputClock = null!;
/// <summary>
/// The last time value where the display was required (due to a significant change or hovering).
/// </summary>
private double lastDisplayRequiredTime;
[Resolved] [Resolved]
private OsuColour colours { get; set; } = null!; private OsuColour colours { get; set; } = null!;
@ -44,7 +65,7 @@ namespace osu.Game.Graphics.UserInterface
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config, GameHost gameHost)
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
@ -77,20 +98,23 @@ namespace osu.Game.Graphics.UserInterface
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
counterUpdateFrameTime = new FrameTimeCounter counterUpdateFrameTime = new OsuSpriteText
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
Margin = new MarginPadding(1), Margin = new MarginPadding(1),
Font = OsuFont.Default.With(fixedWidth: true, size: 16, weight: FontWeight.SemiBold),
Spacing = new Vector2(-1),
Y = -2, Y = -2,
}, },
counterDrawFPS = new FramesPerSecondCounter counterDrawFPS = new OsuSpriteText
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
Margin = new MarginPadding(2), Margin = new MarginPadding(2),
Font = OsuFont.Default.With(fixedWidth: true, size: 13, weight: FontWeight.SemiBold),
Spacing = new Vector2(-2),
Y = 10, Y = 10,
Scale = new Vector2(0.8f),
} }
} }
}, },
@ -99,19 +123,23 @@ namespace osu.Game.Graphics.UserInterface
}; };
config.BindWith(OsuSetting.ShowFpsDisplay, showFpsDisplay); config.BindWith(OsuSetting.ShowFpsDisplay, showFpsDisplay);
drawClock = gameHost.DrawThread.Clock;
updateClock = gameHost.UpdateThread.Clock;
inputClock = gameHost.InputThread.Clock;
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
displayTemporarily(); requestDisplay();
showFpsDisplay.BindValueChanged(showFps => showFpsDisplay.BindValueChanged(showFps =>
{ {
State.Value = showFps.NewValue ? Visibility.Visible : Visibility.Hidden; State.Value = showFps.NewValue ? Visibility.Visible : Visibility.Hidden;
if (showFps.NewValue) if (showFps.NewValue)
displayTemporarily(); requestDisplay();
}, true); }, true);
State.BindValueChanged(state => showFpsDisplay.Value = state.NewValue == Visibility.Visible); State.BindValueChanged(state => showFpsDisplay.Value = state.NewValue == Visibility.Visible);
@ -124,48 +152,17 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
background.FadeTo(1, 200); background.FadeTo(1, 200);
displayTemporarily(); requestDisplay();
return base.OnHover(e); return base.OnHover(e);
} }
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)
{ {
background.FadeTo(idle_background_alpha, 200); background.FadeTo(idle_background_alpha, 200);
displayTemporarily(); requestDisplay();
base.OnHoverLost(e); base.OnHoverLost(e);
} }
private bool isDisplayed;
private ScheduledDelegate? fadeOutDelegate;
private double aimDrawFPS;
private double aimUpdateFPS;
private void displayTemporarily()
{
if (!isDisplayed)
{
mainContent.FadeTo(1, 300, Easing.OutQuint);
isDisplayed = true;
}
fadeOutDelegate?.Cancel();
fadeOutDelegate = null;
if (!IsHovered)
{
fadeOutDelegate = Scheduler.AddDelayed(() =>
{
mainContent.FadeTo(0, 300, Easing.OutQuint);
isDisplayed = false;
}, 2000);
}
}
[Resolved]
private GameHost gameHost { get; set; } = null!;
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
@ -176,50 +173,75 @@ namespace osu.Game.Graphics.UserInterface
// frame limiter (we want to show the FPS as it's changing, even if it isn't an outlier). // frame limiter (we want to show the FPS as it's changing, even if it isn't an outlier).
bool aimRatesChanged = updateAimFPS(); bool aimRatesChanged = updateAimFPS();
// TODO: this is wrong (elapsed clock time, not actual run time). bool hasUpdateSpike = displayedFrameTime < spike_time_ms && updateClock.ElapsedFrameTime > spike_time_ms;
double newUpdateFrameTime = gameHost.UpdateThread.Clock.ElapsedFrameTime;
double newDrawFrameTime = gameHost.DrawThread.Clock.ElapsedFrameTime;
double newDrawFps = gameHost.DrawThread.Clock.FramesPerSecond;
const double spike_time_ms = 20;
bool hasUpdateSpike = counterUpdateFrameTime.Current.Value < spike_time_ms && newUpdateFrameTime > spike_time_ms;
// use elapsed frame time rather then FramesPerSecond to better catch stutter frames. // use elapsed frame time rather then FramesPerSecond to better catch stutter frames.
bool hasDrawSpike = counterDrawFPS.Current.Value > (1000 / spike_time_ms) && newDrawFrameTime > spike_time_ms; bool hasDrawSpike = displayedFpsCount > (1000 / spike_time_ms) && drawClock.ElapsedFrameTime > spike_time_ms;
// If the frame time spikes up, make sure it shows immediately on the counter. // note that we use an elapsed time here of 1 intentionally.
if (hasUpdateSpike) // this weights all updates equally. if we passed in the elapsed time, longer frames would be weighted incorrectly lower.
counterUpdateFrameTime.SetCountWithoutRolling(newUpdateFrameTime); displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, updateClock.ElapsedFrameTime, hasUpdateSpike ? 0 : 100, 1);
else
counterUpdateFrameTime.Current.Value = newUpdateFrameTime;
if (hasDrawSpike) if (hasDrawSpike)
// show spike time using raw elapsed value, to account for `FramesPerSecond` being so averaged spike frames don't show. // show spike time using raw elapsed value, to account for `FramesPerSecond` being so averaged spike frames don't show.
counterDrawFPS.SetCountWithoutRolling(1000 / newDrawFrameTime); displayedFpsCount = 1000 / drawClock.ElapsedFrameTime;
else else
counterDrawFPS.Current.Value = newDrawFps; displayedFpsCount = Interpolation.DampContinuously(displayedFpsCount, drawClock.FramesPerSecond, 100, Time.Elapsed);
counterDrawFPS.Colour = getColour(counterDrawFPS.DisplayedCount / aimDrawFPS); if (Time.Current - lastUpdate > min_time_between_updates)
{
updateFpsDisplay();
updateFrameTimeDisplay();
double displayedUpdateFPS = 1000 / counterUpdateFrameTime.DisplayedCount; lastUpdate = Time.Current;
counterUpdateFrameTime.Colour = getColour(displayedUpdateFPS / aimUpdateFPS); }
bool hasSignificantChanges = aimRatesChanged bool hasSignificantChanges = aimRatesChanged
|| hasDrawSpike || hasDrawSpike
|| hasUpdateSpike || hasUpdateSpike
|| counterDrawFPS.DisplayedCount < aimDrawFPS * 0.8 || displayedFpsCount < aimDrawFPS * 0.8
|| displayedUpdateFPS < aimUpdateFPS * 0.8; || 1000 / displayedFrameTime < aimUpdateFPS * 0.8;
if (hasSignificantChanges) if (hasSignificantChanges)
displayTemporarily(); requestDisplay();
else if (isDisplayed && Time.Current - lastDisplayRequiredTime > 2000)
{
mainContent.FadeTo(0, 300, Easing.OutQuint);
isDisplayed = false;
}
}
private void requestDisplay()
{
lastDisplayRequiredTime = Time.Current;
if (!isDisplayed)
{
mainContent.FadeTo(1, 300, Easing.OutQuint);
isDisplayed = true;
}
}
private void updateFpsDisplay()
{
counterDrawFPS.Colour = getColour(displayedFpsCount / aimDrawFPS);
counterDrawFPS.Text = $"{displayedFpsCount:#,0}fps";
}
private void updateFrameTimeDisplay()
{
counterUpdateFrameTime.Text = displayedFrameTime < 5
? $"{displayedFrameTime:N1}ms"
: $"{displayedFrameTime:N0}ms";
counterUpdateFrameTime.Colour = getColour((1000 / displayedFrameTime) / aimUpdateFPS);
} }
private bool updateAimFPS() private bool updateAimFPS()
{ {
if (gameHost.UpdateThread.Clock.Throttling) if (updateClock.Throttling)
{ {
double newAimDrawFPS = gameHost.DrawThread.Clock.MaximumUpdateHz; double newAimDrawFPS = drawClock.MaximumUpdateHz;
double newAimUpdateFPS = gameHost.UpdateThread.Clock.MaximumUpdateHz; double newAimUpdateFPS = updateClock.MaximumUpdateHz;
if (aimDrawFPS != newAimDrawFPS || aimUpdateFPS != newAimUpdateFPS) if (aimDrawFPS != newAimDrawFPS || aimUpdateFPS != newAimUpdateFPS)
{ {
@ -230,7 +252,7 @@ namespace osu.Game.Graphics.UserInterface
} }
else else
{ {
double newAimFPS = gameHost.InputThread.Clock.MaximumUpdateHz; double newAimFPS = inputClock.MaximumUpdateHz;
if (aimDrawFPS != newAimFPS || aimUpdateFPS != newAimFPS) if (aimDrawFPS != newAimFPS || aimUpdateFPS != newAimFPS)
{ {
@ -253,50 +275,5 @@ namespace osu.Game.Graphics.UserInterface
public ITooltip GetCustomTooltip() => new FPSCounterTooltip(); public ITooltip GetCustomTooltip() => new FPSCounterTooltip();
public object TooltipContent => this; public object TooltipContent => this;
public class FramesPerSecondCounter : RollingCounter<double>
{
protected override double RollingDuration => 1000;
protected override OsuSpriteText CreateSpriteText()
{
return new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Font = OsuFont.Default.With(fixedWidth: true, size: 16, weight: FontWeight.SemiBold),
Spacing = new Vector2(-2),
};
}
protected override LocalisableString FormatCount(double count)
{
return $"{count:#,0}fps";
}
}
public class FrameTimeCounter : RollingCounter<double>
{
protected override double RollingDuration => 1000;
protected override OsuSpriteText CreateSpriteText()
{
return new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Font = OsuFont.Default.With(fixedWidth: true, size: 16, weight: FontWeight.SemiBold),
Spacing = new Vector2(-1),
};
}
protected override LocalisableString FormatCount(double count)
{
if (count < 1)
return $"{count:N1}ms";
return $"{count:N0}ms";
}
}
} }
} }

View File

@ -228,10 +228,8 @@ namespace osu.Game.Graphics.UserInterface
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()
{ {
base.UpdateAfterChildren(); base.UpdateAfterChildren();
LeftBox.Scale = new Vector2(Math.Clamp( LeftBox.Scale = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2, 0, Math.Max(0, DrawWidth)), 1);
RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2, 0, DrawWidth), 1); RightBox.Scale = new Vector2(Math.Clamp(DrawWidth - Nub.DrawPosition.X - RangePadding - Nub.DrawWidth / 2, 0, Math.Max(0, DrawWidth)), 1);
RightBox.Scale = new Vector2(Math.Clamp(
DrawWidth - Nub.DrawPosition.X - RangePadding - Nub.DrawWidth / 2, 0, DrawWidth), 1);
} }
protected override void UpdateValue(float value) protected override void UpdateValue(float value)

View File

@ -24,6 +24,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString ModCustomisation => new TranslatableString(getKey(@"mod_customisation"), @"Mod Customisation"); public static LocalisableString ModCustomisation => new TranslatableString(getKey(@"mod_customisation"), @"Mod Customisation");
/// <summary>
/// "Personal Presets"
/// </summary>
public static LocalisableString PersonalPresets => new TranslatableString(getKey(@"personal_presets"), @"Personal Presets");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -34,6 +34,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString AutoCursorSize => new TranslatableString(getKey(@"auto_cursor_size"), @"Adjust gameplay cursor size based on current beatmap"); public static LocalisableString AutoCursorSize => new TranslatableString(getKey(@"auto_cursor_size"), @"Adjust gameplay cursor size based on current beatmap");
/// <summary>
/// "Show gameplay cursor during touch input"
/// </summary>
public static LocalisableString GameplayCursorDuringTouch => new TranslatableString(getKey(@"gameplay_cursor_during_touch"), @"Show gameplay cursor during touch input");
/// <summary> /// <summary>
/// "Beatmap skins" /// "Beatmap skins"
/// </summary> /// </summary>

View File

@ -1,162 +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.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Users;
namespace osu.Game.Online.API.Requests.Responses
{
public class APIScore : IScoreInfo
{
[JsonProperty(@"score")]
public long TotalScore { get; set; }
[JsonProperty(@"max_combo")]
public int MaxCombo { get; set; }
[JsonProperty(@"user")]
public APIUser User { get; set; }
[JsonProperty(@"id")]
public long OnlineID { get; set; }
[JsonProperty(@"replay")]
public bool HasReplay { get; set; }
[JsonProperty(@"created_at")]
public DateTimeOffset Date { get; set; }
[JsonProperty(@"beatmap")]
[CanBeNull]
public APIBeatmap Beatmap { get; set; }
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
[JsonProperty(@"pp")]
public double? PP { get; set; }
[JsonProperty(@"beatmapset")]
[CanBeNull]
public APIBeatmapSet BeatmapSet
{
set
{
// in the deserialisation case we need to ferry this data across.
// the order of properties returned by the API guarantees that the beatmap is populated by this point.
if (!(Beatmap is APIBeatmap apiBeatmap))
throw new InvalidOperationException("Beatmap set metadata arrived before beatmap metadata in response");
apiBeatmap.BeatmapSet = value;
}
}
[JsonProperty("statistics")]
public Dictionary<string, int> Statistics { get; set; }
[JsonProperty(@"mode_int")]
public int RulesetID { get; set; }
[JsonProperty(@"mods")]
private string[] mods { set => Mods = value.Select(acronym => new APIMod { Acronym = acronym }); }
[NotNull]
public IEnumerable<APIMod> Mods { get; set; } = Array.Empty<APIMod>();
[JsonProperty("rank")]
[JsonConverter(typeof(StringEnumConverter))]
public ScoreRank Rank { get; set; }
/// <summary>
/// Create a <see cref="ScoreInfo"/> from an API score instance.
/// </summary>
/// <param name="rulesets">A ruleset store, used to populate a ruleset instance in the returned score.</param>
/// <param name="beatmap">An optional beatmap, copied into the returned score (for cases where the API does not populate the beatmap).</param>
/// <returns></returns>
public ScoreInfo CreateScoreInfo(RulesetStore rulesets, BeatmapInfo beatmap = null)
{
var ruleset = rulesets.GetRuleset(RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {RulesetID} not found locally");
var rulesetInstance = ruleset.CreateInstance();
var modInstances = Mods.Select(apiMod => rulesetInstance.CreateModFromAcronym(apiMod.Acronym)).Where(m => m != null).ToArray();
// all API scores provided by this class are considered to be legacy.
modInstances = modInstances.Append(rulesetInstance.CreateMod<ModClassic>()).ToArray();
var scoreInfo = new ScoreInfo
{
TotalScore = TotalScore,
MaxCombo = MaxCombo,
BeatmapInfo = beatmap ?? new BeatmapInfo(),
User = User,
Accuracy = Accuracy,
OnlineID = OnlineID,
Date = Date,
PP = PP,
Hash = HasReplay ? "online" : string.Empty, // todo: temporary?
Rank = Rank,
Ruleset = ruleset,
Mods = modInstances,
};
if (Statistics != null)
{
foreach (var kvp in Statistics)
{
switch (kvp.Key)
{
case @"count_geki":
scoreInfo.SetCountGeki(kvp.Value);
break;
case @"count_300":
scoreInfo.SetCount300(kvp.Value);
break;
case @"count_katu":
scoreInfo.SetCountKatu(kvp.Value);
break;
case @"count_100":
scoreInfo.SetCount100(kvp.Value);
break;
case @"count_50":
scoreInfo.SetCount50(kvp.Value);
break;
case @"count_miss":
scoreInfo.SetCountMiss(kvp.Value);
break;
}
}
}
return scoreInfo;
}
public IRulesetInfo Ruleset => new RulesetInfo { OnlineID = RulesetID };
IEnumerable<INamedFileUsage> IHasNamedFiles.Files => throw new NotImplementedException();
#region Implementation of IScoreInfo
IBeatmapInfo IScoreInfo.Beatmap => Beatmap;
IUser IScoreInfo.User => User;
#endregion
}
}

View File

@ -151,6 +151,23 @@ namespace osu.Game.Online.API.Requests.Responses
PP = PP, PP = PP,
}; };
/// <summary>
/// Creates a <see cref="SoloScoreInfo"/> from a local score for score submission.
/// </summary>
/// <param name="score">The local score.</param>
public static SoloScoreInfo ForSubmission(ScoreInfo score) => new SoloScoreInfo
{
Rank = score.Rank,
TotalScore = (int)score.TotalScore,
Accuracy = score.Accuracy,
PP = score.PP,
MaxCombo = score.MaxCombo,
RulesetID = score.RulesetID,
Passed = score.Passed,
Mods = score.APIMods,
Statistics = score.Statistics,
};
public long OnlineID => ID ?? -1; public long OnlineID => ID ?? -1;
} }
} }

View File

@ -7,20 +7,20 @@ using System.Net.Http;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Solo; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Scoring; using osu.Game.Scoring;
namespace osu.Game.Online.Rooms namespace osu.Game.Online.Rooms
{ {
public abstract class SubmitScoreRequest : APIRequest<MultiplayerScore> public abstract class SubmitScoreRequest : APIRequest<MultiplayerScore>
{ {
public readonly SubmittableScore Score; public readonly SoloScoreInfo Score;
protected readonly long ScoreId; protected readonly long ScoreId;
protected SubmitScoreRequest(ScoreInfo scoreInfo, long scoreId) protected SubmitScoreRequest(ScoreInfo scoreInfo, long scoreId)
{ {
Score = new SubmittableScore(scoreInfo); Score = SoloScoreInfo.ForSubmission(scoreInfo);
ScoreId = scoreId; ScoreId = scoreId;
} }

View File

@ -1,72 +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.
#nullable disable
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Online.Solo
{
/// <summary>
/// A class specifically for sending scores to the API during score submission.
/// This is used instead of <see cref="APIScore"/> due to marginally different serialisation naming requirements.
/// </summary>
[Serializable]
public class SubmittableScore
{
[JsonProperty("rank")]
[JsonConverter(typeof(StringEnumConverter))]
public ScoreRank Rank { get; set; }
[JsonProperty("total_score")]
public long TotalScore { get; set; }
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
[JsonProperty(@"pp")]
public double? PP { get; set; }
[JsonProperty("max_combo")]
public int MaxCombo { get; set; }
[JsonProperty("ruleset_id")]
public int RulesetID { get; set; }
[JsonProperty("passed")]
public bool Passed { get; set; }
// Used for API serialisation/deserialisation.
[JsonProperty("mods")]
public APIMod[] Mods { get; set; }
[JsonProperty("statistics")]
public Dictionary<HitResult, int> Statistics { get; set; }
[UsedImplicitly]
public SubmittableScore()
{
}
public SubmittableScore(ScoreInfo score)
{
Rank = score.Rank;
TotalScore = score.TotalScore;
Accuracy = score.Accuracy;
PP = score.PP;
MaxCombo = score.MaxCombo;
RulesetID = score.RulesetID;
Passed = score.Passed;
Mods = score.APIMods;
Statistics = score.Statistics;
}
}
}

View File

@ -206,6 +206,11 @@ namespace osu.Game.Online.Spectator
if (!IsPlaying) if (!IsPlaying)
return; return;
// Disposal can take some time, leading to EndPlaying potentially being called after a future play session.
// Account for this by ensuring the score of the current play matches the one in the provided state.
if (currentScore != state.Score)
return;
if (pendingFrames.Count > 0) if (pendingFrames.Count > 0)
purgePendingFrames(); purgePendingFrames();

View File

@ -716,7 +716,7 @@ namespace osu.Game
// The next time this is updated is in UpdateAfterChildren, which occurs too late and results // The next time this is updated is in UpdateAfterChildren, which occurs too late and results
// in the cursor being shown for a few frames during the intro. // in the cursor being shown for a few frames during the intro.
// This prevents the cursor from showing until we have a screen with CursorVisible = true // This prevents the cursor from showing until we have a screen with CursorVisible = true
MenuCursorContainer.CanShowCursor = menuScreen?.CursorVisible ?? false; GlobalCursorDisplay.ShowCursor = menuScreen?.CursorVisible ?? false;
// todo: all archive managers should be able to be looped here. // todo: all archive managers should be able to be looped here.
SkinManager.PostNotification = n => Notifications.Post(n); SkinManager.PostNotification = n => Notifications.Post(n);
@ -904,6 +904,8 @@ namespace osu.Game
loadComponentSingleFile(CreateHighPerformanceSession(), Add); loadComponentSingleFile(CreateHighPerformanceSession(), Add);
loadComponentSingleFile(new BackgroundBeatmapProcessor(), Add);
chatOverlay.State.BindValueChanged(_ => updateChatPollRate()); chatOverlay.State.BindValueChanged(_ => updateChatPollRate());
// Multiplayer modes need to increase poll rate temporarily. // Multiplayer modes need to increase poll rate temporarily.
API.Activity.BindValueChanged(_ => updateChatPollRate(), true); API.Activity.BindValueChanged(_ => updateChatPollRate(), true);
@ -1229,7 +1231,7 @@ namespace osu.Game
ScreenOffsetContainer.X = horizontalOffset; ScreenOffsetContainer.X = horizontalOffset;
overlayContent.X = horizontalOffset * 1.2f; overlayContent.X = horizontalOffset * 1.2f;
MenuCursorContainer.CanShowCursor = (ScreenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false; GlobalCursorDisplay.ShowCursor = (ScreenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false;
} }
private void screenChanged(IScreen current, IScreen newScreen) private void screenChanged(IScreen current, IScreen newScreen)

View File

@ -138,7 +138,7 @@ namespace osu.Game
protected RealmKeyBindingStore KeyBindingStore { get; private set; } protected RealmKeyBindingStore KeyBindingStore { get; private set; }
protected MenuCursorContainer MenuCursorContainer { get; private set; } protected GlobalCursorDisplay GlobalCursorDisplay { get; private set; }
protected MusicController MusicController { get; private set; } protected MusicController MusicController { get; private set; }
@ -280,8 +280,7 @@ namespace osu.Game
AddInternal(difficultyCache); AddInternal(difficultyCache);
// TODO: OsuGame or OsuGameBase? // TODO: OsuGame or OsuGameBase?
beatmapUpdater = new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage); dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage));
dependencies.CacheAs(spectatorClient = new OnlineSpectatorClient(endpoints)); dependencies.CacheAs(spectatorClient = new OnlineSpectatorClient(endpoints));
dependencies.CacheAs(multiplayerClient = new OnlineMultiplayerClient(endpoints)); dependencies.CacheAs(multiplayerClient = new OnlineMultiplayerClient(endpoints));
dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints)); dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));
@ -341,10 +340,10 @@ namespace osu.Game
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = CreateScalingContainer().WithChildren(new Drawable[] Child = CreateScalingContainer().WithChildren(new Drawable[]
{ {
(MenuCursorContainer = new MenuCursorContainer (GlobalCursorDisplay = new GlobalCursorDisplay
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}).WithChild(content = new OsuTooltipContainer(MenuCursorContainer.Cursor) }).WithChild(content = new OsuTooltipContainer(GlobalCursorDisplay.MenuCursor)
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}), }),

View File

@ -6,7 +6,6 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
@ -25,6 +24,7 @@ using osu.Framework.Localisation;
using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring.Drawables;
namespace osu.Game.Overlays.BeatmapSet.Scores namespace osu.Game.Overlays.BeatmapSet.Scores
{ {
@ -179,8 +179,10 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
if (showPerformancePoints) if (showPerformancePoints)
{ {
Debug.Assert(score.PP != null); if (score.PP != null)
content.Add(new StatisticText(score.PP.Value, format: @"N0")); content.Add(new StatisticText(score.PP, format: @"N0"));
else
content.Add(new UnprocessedPerformancePointsPlaceholder { Size = new Vector2(text_size) });
} }
content.Add(new ScoreboardTime(score.Date, text_size) content.Add(new ScoreboardTime(score.Date, text_size)
@ -222,19 +224,19 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
private class StatisticText : OsuSpriteText, IHasTooltip private class StatisticText : OsuSpriteText, IHasTooltip
{ {
private readonly double count; private readonly double? count;
private readonly double? maxCount; private readonly double? maxCount;
private readonly bool showTooltip; private readonly bool showTooltip;
public LocalisableString TooltipText => maxCount == null || !showTooltip ? string.Empty : $"{count}/{maxCount}"; public LocalisableString TooltipText => maxCount == null || !showTooltip ? string.Empty : $"{count}/{maxCount}";
public StatisticText(double count, double? maxCount = null, string format = null, bool showTooltip = true) public StatisticText(double? count, double? maxCount = null, string format = null, bool showTooltip = true)
{ {
this.count = count; this.count = count;
this.maxCount = maxCount; this.maxCount = maxCount;
this.showTooltip = showTooltip; this.showTooltip = showTooltip;
Text = count.ToLocalisableString(format); Text = count?.ToLocalisableString(format) ?? default;
Font = OsuFont.GetFont(size: text_size); Font = OsuFont.GetFont(size: text_size);
} }

View File

@ -12,14 +12,17 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Scoring.Drawables;
using osuTK; using osuTK;
namespace osu.Game.Overlays.BeatmapSet.Scores namespace osu.Game.Overlays.BeatmapSet.Scores
@ -121,7 +124,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
maxComboColumn.Text = value.MaxCombo.ToLocalisableString(@"0\x"); maxComboColumn.Text = value.MaxCombo.ToLocalisableString(@"0\x");
ppColumn.Alpha = value.BeatmapInfo.Status.GrantsPerformancePoints() ? 1 : 0; ppColumn.Alpha = value.BeatmapInfo.Status.GrantsPerformancePoints() ? 1 : 0;
ppColumn.Text = value.PP?.ToLocalisableString(@"N0") ?? default;
if (value.PP is double pp)
ppColumn.Text = pp.ToLocalisableString(@"N0");
else
ppColumn.Drawable = new UnprocessedPerformancePointsPlaceholder { Size = new Vector2(smallFont.Size) };
statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(createStatisticsColumn); statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(createStatisticsColumn);
modsColumn.Mods = value.Mods; modsColumn.Mods = value.Mods;
@ -197,30 +204,48 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
} }
} }
private class TextColumn : InfoColumn private class TextColumn : InfoColumn, IHasCurrentValue<string>
{ {
private readonly SpriteText text; private readonly OsuTextFlowContainer text;
public TextColumn(LocalisableString title, FontUsage font, float? minWidth = null)
: this(title, new OsuSpriteText { Font = font }, minWidth)
{
}
private TextColumn(LocalisableString title, SpriteText text, float? minWidth = null)
: base(title, text, minWidth)
{
this.text = text;
}
public LocalisableString Text public LocalisableString Text
{ {
set => text.Text = value; set => text.Text = value;
} }
public Drawable Drawable
{
set
{
text.Clear();
text.AddArbitraryDrawable(value);
}
}
private Bindable<string> current;
public Bindable<string> Current public Bindable<string> Current
{ {
get => text.Current; get => current;
set => text.Current = value; set
{
text.Clear();
text.AddText(value.Value, t => t.Current = current = value);
}
}
public TextColumn(LocalisableString title, FontUsage font, float? minWidth = null)
: this(title, new OsuTextFlowContainer(t => t.Font = font)
{
AutoSizeAxes = Axes.Both
}, minWidth)
{
}
private TextColumn(LocalisableString title, OsuTextFlowContainer text, float? minWidth = null)
: base(title, text, minWidth)
{
this.text = text;
} }
} }

View File

@ -3,7 +3,10 @@
#nullable disable #nullable disable
using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -12,6 +15,8 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet; using osu.Game.Overlays.BeatmapSet;
using osu.Game.Overlays.BeatmapSet.Scores; using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Overlays.Comments; using osu.Game.Overlays.Comments;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select.Details;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -25,6 +30,14 @@ namespace osu.Game.Overlays
private readonly Bindable<APIBeatmapSet> beatmapSet = new Bindable<APIBeatmapSet>(); private readonly Bindable<APIBeatmapSet> beatmapSet = new Bindable<APIBeatmapSet>();
/// <remarks>
/// Isolates the beatmap set overlay from the game-wide selected mods bindable
/// to avoid affecting the beatmap details section (i.e. <see cref="AdvancedStats.StatisticRow"/>).
/// </remarks>
[Cached]
[Cached(typeof(IBindable<IReadOnlyList<Mod>>))]
protected readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
public BeatmapSetOverlay() public BeatmapSetOverlay()
: base(OverlayColourScheme.Blue) : base(OverlayColourScheme.Blue)
{ {

View File

@ -60,7 +60,7 @@ namespace osu.Game.Overlays.Mods
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X, AutoSizeAxes = Axes.X,
Masking = true, Masking = true,
CornerRadius = ModPanel.CORNER_RADIUS, CornerRadius = ModSelectPanel.CORNER_RADIUS,
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0), Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0),
Children = new Drawable[] Children = new Drawable[]
{ {
@ -69,7 +69,7 @@ namespace osu.Game.Overlays.Mods
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Width = multiplier_value_area_width + ModPanel.CORNER_RADIUS Width = multiplier_value_area_width + ModSelectPanel.CORNER_RADIUS
}, },
new GridContainer new GridContainer
{ {
@ -89,7 +89,7 @@ namespace osu.Game.Overlays.Mods
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X, AutoSizeAxes = Axes.X,
Masking = true, Masking = true,
CornerRadius = ModPanel.CORNER_RADIUS, CornerRadius = ModSelectPanel.CORNER_RADIUS,
Children = new Drawable[] Children = new Drawable[]
{ {
contentBackground = new Box contentBackground = new Box

View File

@ -12,14 +12,10 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Overlays.Mods.Input; using osu.Game.Overlays.Mods.Input;
@ -29,10 +25,8 @@ using osuTK.Graphics;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public class ModColumn : CompositeDrawable public class ModColumn : ModSelectColumn
{ {
public readonly Container TopLevelContent;
public readonly ModType ModType; public readonly ModType ModType;
private IReadOnlyList<ModState> availableMods = Array.Empty<ModState>(); private IReadOnlyList<ModState> availableMods = Array.Empty<ModState>();
@ -62,149 +56,29 @@ namespace osu.Game.Overlays.Mods
} }
} }
/// <summary>
/// Determines whether this column should accept user input.
/// </summary>
public Bindable<bool> Active = new BindableBool(true);
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value;
protected virtual ModPanel CreateModPanel(ModState mod) => new ModPanel(mod); protected virtual ModPanel CreateModPanel(ModState mod) => new ModPanel(mod);
private readonly bool allowIncompatibleSelection; private readonly bool allowIncompatibleSelection;
private readonly TextFlowContainer headerText;
private readonly Box headerBackground;
private readonly Container contentContainer;
private readonly Box contentBackground;
private readonly FillFlowContainer<ModPanel> panelFlow;
private readonly ToggleAllCheckbox? toggleAllCheckbox; private readonly ToggleAllCheckbox? toggleAllCheckbox;
private Colour4 accentColour;
private Bindable<ModSelectHotkeyStyle> hotkeyStyle = null!; private Bindable<ModSelectHotkeyStyle> hotkeyStyle = null!;
private IModHotkeyHandler hotkeyHandler = null!; private IModHotkeyHandler hotkeyHandler = null!;
private Task? latestLoadTask; private Task? latestLoadTask;
internal bool ItemsLoaded => latestLoadTask == null; internal bool ItemsLoaded => latestLoadTask == null;
private const float header_height = 42;
public ModColumn(ModType modType, bool allowIncompatibleSelection) public ModColumn(ModType modType, bool allowIncompatibleSelection)
{ {
ModType = modType; ModType = modType;
this.allowIncompatibleSelection = allowIncompatibleSelection; this.allowIncompatibleSelection = allowIncompatibleSelection;
Width = 320; HeaderText = ModType.Humanize(LetterCasing.Title);
RelativeSizeAxes = Axes.Y;
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0);
Container controlContainer;
InternalChildren = new Drawable[]
{
TopLevelContent = new Container
{
RelativeSizeAxes = Axes.Both,
CornerRadius = ModPanel.CORNER_RADIUS,
Masking = true,
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
Height = header_height + ModPanel.CORNER_RADIUS,
Children = new Drawable[]
{
headerBackground = new Box
{
RelativeSizeAxes = Axes.X,
Height = header_height + ModPanel.CORNER_RADIUS
},
headerText = new OsuTextFlowContainer(t =>
{
t.Font = OsuFont.TorusAlternate.With(size: 17);
t.Shadow = false;
t.Colour = Colour4.Black;
})
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Padding = new MarginPadding
{
Horizontal = 17,
Bottom = ModPanel.CORNER_RADIUS
}
}
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = header_height },
Child = contentContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = ModPanel.CORNER_RADIUS,
BorderThickness = 3,
Children = new Drawable[]
{
contentBackground = new Box
{
RelativeSizeAxes = Axes.Both
},
new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension()
},
Content = new[]
{
new Drawable[]
{
controlContainer = new Container
{
RelativeSizeAxes = Axes.X,
Padding = new MarginPadding { Horizontal = 14 }
}
},
new Drawable[]
{
new OsuScrollContainer(Direction.Vertical)
{
RelativeSizeAxes = Axes.Both,
ClampExtension = 100,
ScrollbarOverlapsContent = false,
Child = panelFlow = new FillFlowContainer<ModPanel>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0, 7),
Padding = new MarginPadding(7)
}
}
}
}
}
}
}
}
}
}
};
createHeaderText();
if (allowIncompatibleSelection) if (allowIncompatibleSelection)
{ {
controlContainer.Height = 35; ControlContainer.Height = 35;
controlContainer.Add(toggleAllCheckbox = new ToggleAllCheckbox(this) ControlContainer.Add(toggleAllCheckbox = new ToggleAllCheckbox(this)
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
@ -212,7 +86,7 @@ namespace osu.Game.Overlays.Mods
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0) Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0)
}); });
panelFlow.Padding = new MarginPadding ItemsFlow.Padding = new MarginPadding
{ {
Top = 0, Top = 0,
Bottom = 7, Bottom = 7,
@ -221,33 +95,17 @@ namespace osu.Game.Overlays.Mods
} }
} }
private void createHeaderText()
{
IEnumerable<string> headerTextWords = ModType.Humanize(LetterCasing.Title).Split(' ');
if (headerTextWords.Count() > 1)
{
headerText.AddText($"{headerTextWords.First()} ", t => t.Font = t.Font.With(weight: FontWeight.SemiBold));
headerTextWords = headerTextWords.Skip(1);
}
headerText.AddText(string.Join(' ', headerTextWords));
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, OsuColour colours, OsuConfigManager configManager) private void load(OsuColour colours, OsuConfigManager configManager)
{ {
headerBackground.Colour = accentColour = colours.ForModType(ModType); AccentColour = colours.ForModType(ModType);
if (toggleAllCheckbox != null) if (toggleAllCheckbox != null)
{ {
toggleAllCheckbox.AccentColour = accentColour; toggleAllCheckbox.AccentColour = AccentColour;
toggleAllCheckbox.AccentHoverColour = accentColour.Lighten(0.3f); toggleAllCheckbox.AccentHoverColour = AccentColour.Lighten(0.3f);
} }
contentContainer.BorderColour = ColourInfo.GradientVertical(colourProvider.Background4, colourProvider.Background3);
contentBackground.Colour = colourProvider.Background4;
hotkeyStyle = configManager.GetBindable<ModSelectHotkeyStyle>(OsuSetting.ModSelectHotkeyStyle); hotkeyStyle = configManager.GetBindable<ModSelectHotkeyStyle>(OsuSetting.ModSelectHotkeyStyle);
} }
@ -278,7 +136,7 @@ namespace osu.Game.Overlays.Mods
latestLoadTask = loadTask = LoadComponentsAsync(panels, loaded => latestLoadTask = loadTask = LoadComponentsAsync(panels, loaded =>
{ {
panelFlow.ChildrenEnumerable = loaded; ItemsFlow.ChildrenEnumerable = loaded;
updateState(); updateState();
}, (cancellationTokenSource = new CancellationTokenSource()).Token); }, (cancellationTokenSource = new CancellationTokenSource()).Token);
loadTask.ContinueWith(_ => loadTask.ContinueWith(_ =>

View File

@ -1,144 +1,42 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public class ModPanel : OsuClickableContainer public class ModPanel : ModSelectPanel
{ {
public Mod Mod => modState.Mod; public Mod Mod => modState.Mod;
public BindableBool Active => modState.Active; public override BindableBool Active => modState.Active;
public BindableBool Filtered => modState.Filtered; public BindableBool Filtered => modState.Filtered;
protected override float IdleSwitchWidth => 54;
protected override float ExpandedSwitchWidth => 70;
private readonly ModState modState; private readonly ModState modState;
protected readonly Box Background;
protected readonly Container SwitchContainer;
protected readonly Container MainContentContainer;
protected readonly Box TextBackground;
protected readonly FillFlowContainer TextFlow;
[Resolved]
protected OverlayColourProvider ColourProvider { get; private set; } = null!;
protected const double TRANSITION_DURATION = 150;
public const float CORNER_RADIUS = 7;
protected const float HEIGHT = 42;
protected const float IDLE_SWITCH_WIDTH = 54;
protected const float EXPANDED_SWITCH_WIDTH = 70;
private Colour4 activeColour;
private readonly Bindable<bool> samplePlaybackDisabled = new BindableBool();
private Sample? sampleOff;
private Sample? sampleOn;
public ModPanel(ModState modState) public ModPanel(ModState modState)
{ {
this.modState = modState; this.modState = modState;
RelativeSizeAxes = Axes.X; Title = Mod.Name;
Height = 42; Description = Mod.Description;
// all below properties are applied to `Content` rather than the `ModPanel` in its entirety SwitchContainer.Child = new ModSwitchSmall(Mod)
// to allow external components to set these properties on the panel without affecting
// its "internal" appearance.
Content.Masking = true;
Content.CornerRadius = CORNER_RADIUS;
Content.BorderThickness = 2;
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0);
Children = new Drawable[]
{ {
Background = new Box Anchor = Anchor.Centre,
{ Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both Active = { BindTarget = Active },
}, Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
SwitchContainer = new Container Scale = new Vector2(HEIGHT / ModSwitchSmall.DEFAULT_SIZE)
{
RelativeSizeAxes = Axes.Y,
Child = new ModSwitchSmall(Mod)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Active = { BindTarget = Active },
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Scale = new Vector2(HEIGHT / ModSwitchSmall.DEFAULT_SIZE)
}
},
MainContentContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = CORNER_RADIUS,
Children = new Drawable[]
{
TextBackground = new Box
{
RelativeSizeAxes = Axes.Both
},
TextFlow = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Horizontal = 17.5f,
Vertical = 4
},
Direction = FillDirection.Vertical,
Children = new[]
{
new OsuSpriteText
{
Text = Mod.Name,
Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold),
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Margin = new MarginPadding
{
Left = -18 * ShearedOverlayContainer.SHEAR
}
},
new OsuSpriteText
{
Text = Mod.Description,
Font = OsuFont.Default.With(size: 12),
RelativeSizeAxes = Axes.X,
Truncate = true,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0)
}
}
}
}
}
}
}; };
Action = Active.Toggle;
} }
public ModPanel(Mod mod) public ModPanel(Mod mod)
@ -146,122 +44,21 @@ namespace osu.Game.Overlays.Mods
{ {
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader]
private void load(AudioManager audio, OsuColour colours, ISamplePlaybackDisabler? samplePlaybackDisabler) private void load(OsuColour colours)
{ {
sampleOn = audio.Samples.Get(@"UI/check-on"); AccentColour = colours.ForModType(Mod.Type);
sampleOff = audio.Samples.Get(@"UI/check-off");
activeColour = colours.ForModType(Mod.Type);
if (samplePlaybackDisabler != null)
((IBindable<bool>)samplePlaybackDisabled).BindTo(samplePlaybackDisabler.SamplePlaybackDisabled);
} }
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
Active.BindValueChanged(_ =>
{
playStateChangeSamples();
UpdateState();
});
Filtered.BindValueChanged(_ => updateFilterState(), true); Filtered.BindValueChanged(_ => updateFilterState(), true);
UpdateState();
FinishTransforms(true);
}
private void playStateChangeSamples()
{
if (samplePlaybackDisabled.Value)
return;
if (Active.Value)
sampleOn?.Play();
else
sampleOff?.Play();
}
protected override bool OnHover(HoverEvent e)
{
UpdateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
UpdateState();
base.OnHoverLost(e);
}
private bool mouseDown;
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button == MouseButton.Left)
mouseDown = true;
UpdateState();
return false;
}
protected override void OnMouseUp(MouseUpEvent e)
{
mouseDown = false;
UpdateState();
base.OnMouseUp(e);
}
protected virtual Colour4 BackgroundColour => Active.Value ? activeColour.Darken(0.3f) : ColourProvider.Background3;
protected virtual Colour4 ForegroundColour => Active.Value ? activeColour : ColourProvider.Background2;
protected virtual Colour4 TextColour => Active.Value ? ColourProvider.Background6 : Colour4.White;
protected virtual void UpdateState()
{
float targetWidth = Active.Value ? EXPANDED_SWITCH_WIDTH : IDLE_SWITCH_WIDTH;
double transitionDuration = TRANSITION_DURATION;
Colour4 backgroundColour = BackgroundColour;
Colour4 foregroundColour = ForegroundColour;
Colour4 textColour = TextColour;
// Hover affects colour of button background
if (IsHovered)
{
backgroundColour = backgroundColour.Lighten(0.1f);
foregroundColour = foregroundColour.Lighten(0.1f);
}
// Mouse down adds a halfway tween of the movement
if (mouseDown)
{
targetWidth = (float)Interpolation.Lerp(IDLE_SWITCH_WIDTH, EXPANDED_SWITCH_WIDTH, 0.5f);
transitionDuration *= 4;
}
Content.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(backgroundColour, foregroundColour), transitionDuration, Easing.OutQuint);
Background.FadeColour(backgroundColour, transitionDuration, Easing.OutQuint);
SwitchContainer.ResizeWidthTo(targetWidth, transitionDuration, Easing.OutQuint);
MainContentContainer.TransformTo(nameof(Padding), new MarginPadding
{
Left = targetWidth,
Right = CORNER_RADIUS
}, transitionDuration, Easing.OutQuint);
TextBackground.FadeColour(foregroundColour, transitionDuration, Easing.OutQuint);
TextFlow.FadeColour(textColour, transitionDuration, Easing.OutQuint);
} }
#region Filtering support #region Filtering support
public void ApplyFilter(Func<Mod, bool>? filter)
{
Filtered.Value = filter != null && !filter.Invoke(Mod);
}
private void updateFilterState() private void updateFilterState()
{ {
this.FadeTo(Filtered.Value ? 0 : 1); this.FadeTo(Filtered.Value ? 0 : 1);

View File

@ -0,0 +1,77 @@
// 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 System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Rulesets.Mods;
using osuTK;
namespace osu.Game.Overlays.Mods
{
public class ModPresetColumn : ModSelectColumn
{
private IReadOnlyList<ModPreset> presets = Array.Empty<ModPreset>();
/// <summary>
/// Sets the collection of available mod presets.
/// </summary>
public IReadOnlyList<ModPreset> Presets
{
get => presets;
set
{
presets = value;
if (IsLoaded)
asyncLoadPanels();
}
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AccentColour = colours.Orange1;
HeaderText = ModSelectOverlayStrings.PersonalPresets;
}
protected override void LoadComplete()
{
base.LoadComplete();
asyncLoadPanels();
}
private CancellationTokenSource? cancellationTokenSource;
private Task? latestLoadTask;
internal bool ItemsLoaded => latestLoadTask == null;
private void asyncLoadPanels()
{
cancellationTokenSource?.Cancel();
var panels = presets.Select(preset => new ModPresetPanel(preset)
{
Shear = Vector2.Zero
});
Task? loadTask;
latestLoadTask = loadTask = LoadComponentsAsync(panels, loaded =>
{
ItemsFlow.ChildrenEnumerable = loaded;
}, (cancellationTokenSource = new CancellationTokenSource()).Token);
loadTask.ContinueWith(_ =>
{
if (loadTask == latestLoadTask)
latestLoadTask = null;
});
}
}
}

View File

@ -0,0 +1,35 @@
// 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.Bindables;
using osu.Framework.Graphics.Cursor;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Overlays.Mods
{
public class ModPresetPanel : ModSelectPanel, IHasCustomTooltip<ModPreset>
{
public readonly ModPreset Preset;
public override BindableBool Active { get; } = new BindableBool();
public ModPresetPanel(ModPreset preset)
{
Preset = preset;
Title = preset.Name;
Description = preset.Description;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AccentColour = colours.Orange1;
}
public ModPreset TooltipContent => Preset;
public ITooltip<ModPreset> GetCustomTooltip() => new ModPresetTooltip(ColourProvider);
}
}

View File

@ -0,0 +1,115 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Overlays.Mods
{
public class ModPresetTooltip : VisibilityContainer, ITooltip<ModPreset>
{
protected override Container<Drawable> Content { get; }
private const double transition_duration = 200;
public ModPresetTooltip(OverlayColourProvider colourProvider)
{
Width = 250;
AutoSizeAxes = Axes.Y;
Masking = true;
CornerRadius = 7;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background6
},
Content = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(7),
Spacing = new Vector2(7)
}
};
}
private ModPreset? lastPreset;
public void SetContent(ModPreset preset)
{
if (preset == lastPreset)
return;
lastPreset = preset;
Content.ChildrenEnumerable = preset.Mods.Select(mod => new ModPresetRow(mod));
}
protected override void PopIn() => this.FadeIn(transition_duration, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(transition_duration, Easing.OutQuint);
public void Move(Vector2 pos) => Position = pos;
private class ModPresetRow : FillFlowContainer
{
public ModPresetRow(Mod mod)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical;
Spacing = new Vector2(4);
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(7),
Children = new Drawable[]
{
new ModSwitchTiny(mod)
{
Active = { Value = true },
Scale = new Vector2(0.6f),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
new OsuSpriteText
{
Text = mod.Name,
Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold),
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Margin = new MarginPadding { Bottom = 2 }
}
}
}
};
if (!string.IsNullOrEmpty(mod.SettingDescription))
{
AddInternal(new OsuTextFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = 14 },
Text = mod.SettingDescription
});
}
}
}
}
}

View File

@ -0,0 +1,177 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Mods
{
public abstract class ModSelectColumn : CompositeDrawable, IHasAccentColour
{
public readonly Container TopLevelContent;
public LocalisableString HeaderText
{
set => createHeaderText(value);
}
public Color4 AccentColour
{
get => headerBackground.Colour;
set => headerBackground.Colour = value;
}
/// <summary>
/// Determines whether this column should accept user input.
/// </summary>
public readonly Bindable<bool> Active = new BindableBool(true);
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value;
protected readonly Container ControlContainer;
protected readonly FillFlowContainer ItemsFlow;
private readonly TextFlowContainer headerText;
private readonly Box headerBackground;
private readonly Container contentContainer;
private readonly Box contentBackground;
private const float header_height = 42;
protected ModSelectColumn()
{
Width = 320;
RelativeSizeAxes = Axes.Y;
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0);
InternalChildren = new Drawable[]
{
TopLevelContent = new Container
{
RelativeSizeAxes = Axes.Both,
CornerRadius = ModSelectPanel.CORNER_RADIUS,
Masking = true,
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
Height = header_height + ModSelectPanel.CORNER_RADIUS,
Children = new Drawable[]
{
headerBackground = new Box
{
RelativeSizeAxes = Axes.X,
Height = header_height + ModSelectPanel.CORNER_RADIUS
},
headerText = new OsuTextFlowContainer(t =>
{
t.Font = OsuFont.TorusAlternate.With(size: 17);
t.Shadow = false;
t.Colour = Colour4.Black;
})
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Padding = new MarginPadding
{
Horizontal = 17,
Bottom = ModSelectPanel.CORNER_RADIUS
}
}
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = header_height },
Child = contentContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = ModSelectPanel.CORNER_RADIUS,
BorderThickness = 3,
Children = new Drawable[]
{
contentBackground = new Box
{
RelativeSizeAxes = Axes.Both
},
new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension()
},
Content = new[]
{
new Drawable[]
{
ControlContainer = new Container
{
RelativeSizeAxes = Axes.X,
Padding = new MarginPadding { Horizontal = 14 }
}
},
new Drawable[]
{
new OsuScrollContainer(Direction.Vertical)
{
RelativeSizeAxes = Axes.Both,
ClampExtension = 100,
ScrollbarOverlapsContent = false,
Child = ItemsFlow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0, 7),
Padding = new MarginPadding(7)
}
}
}
}
}
}
}
}
}
}
};
}
private void createHeaderText(LocalisableString text)
{
headerText.Clear();
int wordIndex = 0;
headerText.AddText(text, t =>
{
if (wordIndex == 0)
t.Font = t.Font.With(weight: FontWeight.SemiBold);
wordIndex += 1;
});
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
contentContainer.BorderColour = ColourInfo.GradientVertical(colourProvider.Background4, colourProvider.Background3);
contentBackground.Colour = colourProvider.Background4;
}
}
}

View File

@ -0,0 +1,252 @@
// 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.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Overlays.Mods
{
public abstract class ModSelectPanel : OsuClickableContainer, IHasAccentColour
{
public abstract BindableBool Active { get; }
public Color4 AccentColour { get; set; }
public LocalisableString Title
{
get => titleText.Text;
set => titleText.Text = value;
}
public LocalisableString Description
{
get => descriptionText.Text;
set => descriptionText.Text = value;
}
public const float CORNER_RADIUS = 7;
protected const float HEIGHT = 42;
protected virtual float IdleSwitchWidth => 14;
protected virtual float ExpandedSwitchWidth => 30;
protected virtual Colour4 BackgroundColour => Active.Value ? AccentColour.Darken(0.3f) : ColourProvider.Background3;
protected virtual Colour4 ForegroundColour => Active.Value ? AccentColour : ColourProvider.Background2;
protected virtual Colour4 TextColour => Active.Value ? ColourProvider.Background6 : Colour4.White;
protected const double TRANSITION_DURATION = 150;
protected readonly Box Background;
protected readonly Container SwitchContainer;
protected readonly Container MainContentContainer;
protected readonly Box TextBackground;
protected readonly FillFlowContainer TextFlow;
[Resolved]
protected OverlayColourProvider ColourProvider { get; private set; } = null!;
private readonly OsuSpriteText titleText;
private readonly OsuSpriteText descriptionText;
private readonly Bindable<bool> samplePlaybackDisabled = new BindableBool();
private Sample? sampleOff;
private Sample? sampleOn;
protected ModSelectPanel()
{
RelativeSizeAxes = Axes.X;
Height = HEIGHT;
// all below properties are applied to `Content` rather than the `ModPanel` in its entirety
// to allow external components to set these properties on the panel without affecting
// its "internal" appearance.
Content.Masking = true;
Content.CornerRadius = CORNER_RADIUS;
Content.BorderThickness = 2;
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0);
Children = new Drawable[]
{
Background = new Box
{
RelativeSizeAxes = Axes.Both
},
SwitchContainer = new Container
{
RelativeSizeAxes = Axes.Y,
},
MainContentContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = CORNER_RADIUS,
Children = new Drawable[]
{
TextBackground = new Box
{
RelativeSizeAxes = Axes.Both
},
TextFlow = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Horizontal = 17.5f,
Vertical = 4
},
Direction = FillDirection.Vertical,
Children = new[]
{
titleText = new OsuSpriteText
{
Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
Truncate = true,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Margin = new MarginPadding
{
Left = -18 * ShearedOverlayContainer.SHEAR
}
},
descriptionText = new OsuSpriteText
{
Font = OsuFont.Default.With(size: 12),
RelativeSizeAxes = Axes.X,
Truncate = true,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0)
}
}
}
}
}
}
};
Action = () => Active.Toggle();
}
[BackgroundDependencyLoader]
private void load(AudioManager audio, ISamplePlaybackDisabler? samplePlaybackDisabler)
{
sampleOn = audio.Samples.Get(@"UI/check-on");
sampleOff = audio.Samples.Get(@"UI/check-off");
if (samplePlaybackDisabler != null)
((IBindable<bool>)samplePlaybackDisabled).BindTo(samplePlaybackDisabler.SamplePlaybackDisabled);
}
protected sealed override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
protected override void LoadComplete()
{
base.LoadComplete();
Active.BindValueChanged(_ =>
{
playStateChangeSamples();
UpdateState();
});
UpdateState();
FinishTransforms(true);
}
private void playStateChangeSamples()
{
if (samplePlaybackDisabled.Value)
return;
if (Active.Value)
sampleOn?.Play();
else
sampleOff?.Play();
}
protected override bool OnHover(HoverEvent e)
{
UpdateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
UpdateState();
base.OnHoverLost(e);
}
private bool mouseDown;
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button == MouseButton.Left)
mouseDown = true;
UpdateState();
return false;
}
protected override void OnMouseUp(MouseUpEvent e)
{
mouseDown = false;
UpdateState();
base.OnMouseUp(e);
}
protected virtual void UpdateState()
{
float targetWidth = Active.Value ? ExpandedSwitchWidth : IdleSwitchWidth;
double transitionDuration = TRANSITION_DURATION;
Colour4 backgroundColour = BackgroundColour;
Colour4 foregroundColour = ForegroundColour;
Colour4 textColour = TextColour;
// Hover affects colour of button background
if (IsHovered)
{
backgroundColour = backgroundColour.Lighten(0.1f);
foregroundColour = foregroundColour.Lighten(0.1f);
}
// Mouse down adds a halfway tween of the movement
if (mouseDown)
{
targetWidth = (float)Interpolation.Lerp(IdleSwitchWidth, ExpandedSwitchWidth, 0.5f);
transitionDuration *= 4;
}
Content.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(backgroundColour, foregroundColour), transitionDuration, Easing.OutQuint);
Background.FadeColour(backgroundColour, transitionDuration, Easing.OutQuint);
SwitchContainer.ResizeWidthTo(targetWidth, transitionDuration, Easing.OutQuint);
MainContentContainer.TransformTo(nameof(Padding), new MarginPadding
{
Left = targetWidth,
Right = CORNER_RADIUS
}, transitionDuration, Easing.OutQuint);
TextBackground.FadeColour(foregroundColour, transitionDuration, Easing.OutQuint);
TextFlow.FadeColour(textColour, transitionDuration, Easing.OutQuint);
}
}
}

View File

@ -19,6 +19,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards; using osu.Game.Online.Leaderboards;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Scoring.Drawables;
using osu.Game.Utils; using osu.Game.Utils;
using osuTK; using osuTK;
@ -218,39 +219,42 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
private Drawable createDrawablePerformance() private Drawable createDrawablePerformance()
{ {
if (Score.PP.HasValue) if (!Score.PP.HasValue)
{ {
return new FillFlowContainer if (Score.Beatmap?.Status.GrantsPerformancePoints() == true)
return new UnprocessedPerformancePointsPlaceholder { Size = new Vector2(16), Colour = colourProvider.Highlight1 };
return new OsuSpriteText
{ {
AutoSizeAxes = Axes.Both, Font = OsuFont.GetFont(weight: FontWeight.Bold),
Direction = FillDirection.Horizontal, Text = "-",
Children = new[] Colour = colourProvider.Highlight1
{
new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.GetFont(weight: FontWeight.Bold),
Text = $"{Score.PP:0}",
Colour = colourProvider.Highlight1
},
new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
Text = "pp",
Colour = colourProvider.Light3
}
}
}; };
} }
return new OsuSpriteText return new FillFlowContainer
{ {
Font = OsuFont.GetFont(weight: FontWeight.Bold), AutoSizeAxes = Axes.Both,
Text = "-", Direction = FillDirection.Horizontal,
Colour = colourProvider.Highlight1 Children = new[]
{
new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.GetFont(weight: FontWeight.Bold),
Text = $"{Score.PP:0}",
Colour = colourProvider.Highlight1
},
new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
Text = "pp",
Colour = colourProvider.Light3
}
}
}; };
} }

View File

@ -42,12 +42,11 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
CreateDrawableAccuracy(), CreateDrawableAccuracy(),
new Container new Container
{ {
AutoSizeAxes = Axes.Y, Size = new Vector2(50, 14),
Width = 50,
Child = new OsuSpriteText Child = new OsuSpriteText
{ {
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true), Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true),
Text = $"{Score.PP * weight:0}pp", Text = Score.PP.HasValue ? $"{Score.PP * weight:0}pp" : string.Empty,
}, },
} }
} }

View File

@ -32,6 +32,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
LabelText = SkinSettingsStrings.AutoCursorSize, LabelText = SkinSettingsStrings.AutoCursorSize,
Current = config.GetBindable<bool>(OsuSetting.AutoCursorSize) Current = config.GetBindable<bool>(OsuSetting.AutoCursorSize)
}, },
new SettingsCheckbox
{
LabelText = SkinSettingsStrings.GameplayCursorDuringTouch,
Current = config.GetBindable<bool>(OsuSetting.GameplayCursorDuringTouch)
},
}; };
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)

View File

@ -34,6 +34,11 @@ namespace osu.Game.Rulesets.Difficulty
private readonly IRulesetInfo ruleset; private readonly IRulesetInfo ruleset;
private readonly IWorkingBeatmap beatmap; private readonly IWorkingBeatmap beatmap;
/// <summary>
/// A yymmdd version which is used to discern when reprocessing is required.
/// </summary>
public virtual int Version => 0;
protected DifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) protected DifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
{ {
this.ruleset = ruleset; this.ruleset = ruleset;

View File

@ -0,0 +1,35 @@
// 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;
namespace osu.Game.Rulesets.Mods
{
/// <summary>
/// A mod preset is a named collection of configured mods.
/// Presets are presented to the user in the mod select overlay for convenience.
/// </summary>
public class ModPreset
{
/// <summary>
/// The ruleset that the preset is valid for.
/// </summary>
public RulesetInfo RulesetInfo { get; set; } = null!;
/// <summary>
/// The name of the mod preset.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// The description of the mod preset.
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// The set of configured mods that are part of the preset.
/// </summary>
public ICollection<Mod> Mods { get; set; } = Array.Empty<Mod>();
}
}

View File

@ -4,6 +4,7 @@
using System; using System;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets.Difficulty;
using Realms; using Realms;
namespace osu.Game.Rulesets namespace osu.Game.Rulesets
@ -22,6 +23,11 @@ namespace osu.Game.Rulesets
public string InstantiationInfo { get; set; } = string.Empty; public string InstantiationInfo { get; set; } = string.Empty;
/// <summary>
/// Stores the last applied <see cref="DifficultyCalculator.Version"/>
/// </summary>
public int LastAppliedDifficultyVersion { get; set; }
public RulesetInfo(string shortName, string name, string instantiationInfo, int onlineID) public RulesetInfo(string shortName, string name, string instantiationInfo, int onlineID)
{ {
ShortName = shortName; ShortName = shortName;
@ -86,7 +92,8 @@ namespace osu.Game.Rulesets
Name = Name, Name = Name,
ShortName = ShortName, ShortName = ShortName,
InstantiationInfo = InstantiationInfo, InstantiationInfo = InstantiationInfo,
Available = Available Available = Available,
LastAppliedDifficultyVersion = LastAppliedDifficultyVersion,
}; };
public Ruleset CreateInstance() public Ruleset CreateInstance()

View File

@ -126,6 +126,9 @@ namespace osu.Game.Rulesets.Scoring
private bool beatmapApplied; private bool beatmapApplied;
private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>(); private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>();
private Dictionary<HitResult, int>? maximumResultCounts;
private readonly List<HitEvent> hitEvents = new List<HitEvent>(); private readonly List<HitEvent> hitEvents = new List<HitEvent>();
private HitObject? lastHitObject; private HitObject? lastHitObject;
@ -410,12 +413,16 @@ namespace osu.Game.Rulesets.Scoring
{ {
base.Reset(storeResults); base.Reset(storeResults);
scoreResultCounts.Clear();
hitEvents.Clear(); hitEvents.Clear();
lastHitObject = null; lastHitObject = null;
if (storeResults) if (storeResults)
{
maximumScoringValues = currentScoringValues; maximumScoringValues = currentScoringValues;
maximumResultCounts = new Dictionary<HitResult, int>(scoreResultCounts);
}
scoreResultCounts.Clear();
currentScoringValues = default; currentScoringValues = default;
currentMaximumScoringValues = default; currentMaximumScoringValues = default;
@ -423,6 +430,7 @@ namespace osu.Game.Rulesets.Scoring
TotalScore.Value = 0; TotalScore.Value = 0;
Accuracy.Value = 1; Accuracy.Value = 1;
Combo.Value = 0; Combo.Value = 0;
Rank.Disabled = false;
Rank.Value = ScoreRank.X; Rank.Value = ScoreRank.X;
HighestCombo.Value = 0; HighestCombo.Value = 0;
} }
@ -445,6 +453,36 @@ namespace osu.Game.Rulesets.Scoring
score.TotalScore = (long)Math.Round(ComputeFinalScore(ScoringMode.Standardised, score)); score.TotalScore = (long)Math.Round(ComputeFinalScore(ScoringMode.Standardised, score));
} }
/// <summary>
/// Populates the given score with remaining statistics as "missed" and marks it with <see cref="ScoreRank.F"/> rank.
/// </summary>
public void FailScore(ScoreInfo score)
{
if (Rank.Value == ScoreRank.F)
return;
score.Passed = false;
Rank.Value = ScoreRank.F;
Debug.Assert(maximumResultCounts != null);
if (maximumResultCounts.TryGetValue(HitResult.LargeTickHit, out int maximumLargeTick))
scoreResultCounts[HitResult.LargeTickMiss] = maximumLargeTick - scoreResultCounts.GetValueOrDefault(HitResult.LargeTickHit);
if (maximumResultCounts.TryGetValue(HitResult.SmallTickHit, out int maximumSmallTick))
scoreResultCounts[HitResult.SmallTickMiss] = maximumSmallTick - scoreResultCounts.GetValueOrDefault(HitResult.SmallTickHit);
int maximumBonusOrIgnore = maximumResultCounts.Where(kvp => kvp.Key.IsBonus() || kvp.Key == HitResult.IgnoreHit).Sum(kvp => kvp.Value);
int currentBonusOrIgnore = scoreResultCounts.Where(kvp => kvp.Key.IsBonus() || kvp.Key == HitResult.IgnoreHit).Sum(kvp => kvp.Value);
scoreResultCounts[HitResult.IgnoreMiss] = maximumBonusOrIgnore - currentBonusOrIgnore;
int maximumBasic = maximumResultCounts.SingleOrDefault(kvp => kvp.Key.IsBasic()).Value;
int currentBasic = scoreResultCounts.Where(kvp => kvp.Key.IsBasic() && kvp.Key != HitResult.Miss).Sum(kvp => kvp.Value);
scoreResultCounts[HitResult.Miss] = maximumBasic - currentBasic;
PopulateScore(score);
}
public override void ResetFromReplayFrame(ReplayFrame frame) public override void ResetFromReplayFrame(ReplayFrame frame)
{ {
base.ResetFromReplayFrame(frame); base.ResetFromReplayFrame(frame);

View File

@ -380,7 +380,7 @@ namespace osu.Game.Rulesets.UI
// only show the cursor when within the playfield, by default. // only show the cursor when within the playfield, by default.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Playfield.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Playfield.ReceivePositionalInputAt(screenSpacePos);
CursorContainer IProvideCursor.Cursor => Playfield.Cursor; CursorContainer IProvideCursor.MenuCursor => Playfield.Cursor;
public override GameplayCursorContainer Cursor => Playfield.Cursor; public override GameplayCursorContainer Cursor => Playfield.Cursor;

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Scoring.Drawables
{
/// <summary>
/// A placeholder used in PP columns for scores with unprocessed PP value.
/// </summary>
public class UnprocessedPerformancePointsPlaceholder : SpriteIcon, IHasTooltip
{
public LocalisableString TooltipText => ScoresStrings.StatusProcessing;
public UnprocessedPerformancePointsPlaceholder()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Icon = FontAwesome.Solid.ExclamationTriangle;
}
}
}

View File

@ -12,6 +12,7 @@ using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -172,6 +173,10 @@ namespace osu.Game.Scoring
// We can compute the max combo locally after the async beatmap difficulty computation. // We can compute the max combo locally after the async beatmap difficulty computation.
var difficulty = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false); var difficulty = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
if (difficulty == null)
Logger.Log($"Couldn't get beatmap difficulty for beatmap {score.BeatmapInfo.OnlineID}");
return difficulty?.MaxCombo; return difficulty?.MaxCombo;
} }

View File

@ -41,6 +41,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved] [Resolved]
private EditorClock editorClock { get; set; } private EditorClock editorClock { get; set; }
[Resolved]
private EditorBeatmap editorBeatmap { get; set; }
/// <summary> /// <summary>
/// The timeline's scroll position in the last frame. /// The timeline's scroll position in the last frame.
/// </summary> /// </summary>
@ -68,8 +71,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
/// </summary> /// </summary>
private float defaultTimelineZoom; private float defaultTimelineZoom;
private readonly Bindable<double> timelineZoomScale = new BindableDouble(1.0);
public Timeline(Drawable userContent) public Timeline(Drawable userContent)
{ {
this.userContent = userContent; this.userContent = userContent;
@ -93,7 +94,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private Bindable<float> waveformOpacity; private Bindable<float> waveformOpacity;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, EditorBeatmap editorBeatmap, OsuColour colours, OsuConfigManager config) private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours, OsuConfigManager config)
{ {
CentreMarker centreMarker; CentreMarker centreMarker;
@ -145,21 +146,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
waveform.Waveform = b.NewValue.Waveform; waveform.Waveform = b.NewValue.Waveform;
track = b.NewValue.Track; track = b.NewValue.Track;
// todo: i don't think this is safe, the track may not be loaded yet. setupTimelineZoom();
if (track.Length > 0)
{
MaxZoom = getZoomLevelForVisibleMilliseconds(500);
MinZoom = getZoomLevelForVisibleMilliseconds(10000);
defaultTimelineZoom = getZoomLevelForVisibleMilliseconds(6000);
}
}, true); }, true);
timelineZoomScale.Value = editorBeatmap.BeatmapInfo.TimelineZoom; Zoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom);
timelineZoomScale.BindValueChanged(scale =>
{
Zoom = (float)(defaultTimelineZoom * scale.NewValue);
editorBeatmap.BeatmapInfo.TimelineZoom = scale.NewValue;
}, true);
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -209,6 +199,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
scrollToTrackTime(); scrollToTrackTime();
} }
private void setupTimelineZoom()
{
if (!track.IsLoaded)
{
Scheduler.AddOnce(setupTimelineZoom);
return;
}
defaultTimelineZoom = getZoomLevelForVisibleMilliseconds(6000);
float initialZoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom);
SetupZoom(initialZoom, getZoomLevelForVisibleMilliseconds(10000), getZoomLevelForVisibleMilliseconds(500));
}
protected override bool OnScroll(ScrollEvent e) protected override bool OnScroll(ScrollEvent e)
{ {
// if this is not a precision scroll event, let the editor handle the seek itself (for snapping support) // if this is not a precision scroll event, let the editor handle the seek itself (for snapping support)
@ -221,7 +225,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override void OnZoomChanged() protected override void OnZoomChanged()
{ {
base.OnZoomChanged(); base.OnZoomChanged();
timelineZoomScale.Value = Zoom / defaultTimelineZoom; editorBeatmap.BeatmapInfo.TimelineZoom = Zoom / defaultTimelineZoom;
} }
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()

View File

@ -118,7 +118,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Height = 0.5f, Height = 0.5f,
Icon = FontAwesome.Solid.SearchPlus, Icon = FontAwesome.Solid.SearchPlus,
Action = () => changeZoom(1) Action = () => Timeline.AdjustZoomRelatively(1)
}, },
new TimelineButton new TimelineButton
{ {
@ -127,7 +127,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Height = 0.5f, Height = 0.5f,
Icon = FontAwesome.Solid.SearchMinus, Icon = FontAwesome.Solid.SearchMinus,
Action = () => changeZoom(-1) Action = () => Timeline.AdjustZoomRelatively(-1)
}, },
} }
} }
@ -153,7 +153,5 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Timeline.ControlPointsVisible.BindTo(controlPointsCheckbox.Current); Timeline.ControlPointsVisible.BindTo(controlPointsCheckbox.Current);
Timeline.TicksVisible.BindTo(ticksCheckbox.Current); Timeline.TicksVisible.BindTo(ticksCheckbox.Current);
} }
private void changeZoom(float change) => Timeline.Zoom += change;
} }
} }

View File

@ -32,20 +32,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private readonly Container zoomedContent; private readonly Container zoomedContent;
protected override Container<Drawable> Content => zoomedContent; protected override Container<Drawable> Content => zoomedContent;
private float currentZoom = 1;
/// <summary> /// <summary>
/// The current zoom level of <see cref="ZoomableScrollContainer" />. /// The current zoom level of <see cref="ZoomableScrollContainer"/>.
/// It may differ from <see cref="Zoom" /> during transitions. /// It may differ from <see cref="Zoom"/> during transitions.
/// </summary> /// </summary>
public float CurrentZoom => currentZoom; public float CurrentZoom { get; private set; } = 1;
private bool isZoomSetUp;
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private IFrameBasedClock editorClock { get; set; } private IFrameBasedClock editorClock { get; set; }
private readonly LayoutValue zoomedContentWidthCache = new LayoutValue(Invalidation.DrawSize); private readonly LayoutValue zoomedContentWidthCache = new LayoutValue(Invalidation.DrawSize);
public ZoomableScrollContainer() private float minZoom;
private float maxZoom;
/// <summary>
/// Creates a <see cref="ZoomableScrollContainer"/> with no zoom range.
/// Functionality will be disabled until zoom is set up via <see cref="SetupZoom"/>.
/// </summary>
protected ZoomableScrollContainer()
: base(Direction.Horizontal) : base(Direction.Horizontal)
{ {
base.Content.Add(zoomedContent = new Container { RelativeSizeAxes = Axes.Y }); base.Content.Add(zoomedContent = new Container { RelativeSizeAxes = Axes.Y });
@ -53,46 +61,36 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
AddLayout(zoomedContentWidthCache); AddLayout(zoomedContentWidthCache);
} }
private float minZoom = 1;
/// <summary> /// <summary>
/// The minimum zoom level allowed. /// Creates a <see cref="ZoomableScrollContainer"/> with a defined zoom range.
/// </summary> /// </summary>
public float MinZoom public ZoomableScrollContainer(float minimum, float maximum, float initial)
: this()
{ {
get => minZoom; SetupZoom(initial, minimum, maximum);
set
{
if (value < 1)
throw new ArgumentException($"{nameof(MinZoom)} must be >= 1.", nameof(value));
minZoom = value;
// ensure zoom range is in valid state before updating zoom.
if (MinZoom < MaxZoom)
updateZoom();
}
} }
private float maxZoom = 60;
/// <summary> /// <summary>
/// The maximum zoom level allowed. /// Sets up the minimum and maximum range of this zoomable scroll container, along with the initial zoom value.
/// </summary> /// </summary>
public float MaxZoom /// <param name="initial">The initial zoom value, applied immediately.</param>
/// <param name="minimum">The minimum zoom value.</param>
/// <param name="maximum">The maximum zoom value.</param>
protected void SetupZoom(float initial, float minimum, float maximum)
{ {
get => maxZoom; if (minimum < 1)
set throw new ArgumentException($"{nameof(minimum)} ({minimum}) must be >= 1.", nameof(maximum));
{
if (value < 1)
throw new ArgumentException($"{nameof(MaxZoom)} must be >= 1.", nameof(value));
maxZoom = value; if (maximum < 1)
throw new ArgumentException($"{nameof(maximum)} ({maximum}) must be >= 1.", nameof(maximum));
// ensure zoom range is in valid state before updating zoom. if (minimum > maximum)
if (MaxZoom > MinZoom) throw new ArgumentException($"{nameof(minimum)} ({minimum}) must be less than {nameof(maximum)} ({maximum})");
updateZoom();
} minZoom = minimum;
maxZoom = maximum;
CurrentZoom = zoomTarget = initial;
isZoomSetUp = true;
} }
/// <summary> /// <summary>
@ -104,14 +102,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
set => updateZoom(value); set => updateZoom(value);
} }
private void updateZoom(float? value = null) private void updateZoom(float value)
{ {
float newZoom = Math.Clamp(value ?? Zoom, MinZoom, MaxZoom); if (!isZoomSetUp)
return;
float newZoom = Math.Clamp(value, minZoom, maxZoom);
if (IsLoaded) if (IsLoaded)
setZoomTarget(newZoom, ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), zoomedContent).X); setZoomTarget(newZoom, ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), zoomedContent).X);
else else
currentZoom = zoomTarget = newZoom; CurrentZoom = zoomTarget = newZoom;
} }
protected override void Update() protected override void Update()
@ -127,7 +128,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (e.AltPressed) if (e.AltPressed)
{ {
// zoom when holding alt. // zoom when holding alt.
setZoomTarget(zoomTarget + e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X); AdjustZoomRelatively(e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X);
return true; return true;
} }
@ -141,16 +142,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updateZoomedContentWidth() private void updateZoomedContentWidth()
{ {
zoomedContent.Width = DrawWidth * currentZoom; zoomedContent.Width = DrawWidth * CurrentZoom;
zoomedContentWidthCache.Validate(); zoomedContentWidthCache.Validate();
} }
public void AdjustZoomRelatively(float change, float? focusPoint = null)
{
if (!isZoomSetUp)
return;
const float zoom_change_sensitivity = 0.02f;
setZoomTarget(zoomTarget + change * (maxZoom - minZoom) * zoom_change_sensitivity, focusPoint);
}
private float zoomTarget = 1; private float zoomTarget = 1;
private void setZoomTarget(float newZoom, float focusPoint) private void setZoomTarget(float newZoom, float? focusPoint = null)
{ {
zoomTarget = Math.Clamp(newZoom, MinZoom, MaxZoom); zoomTarget = Math.Clamp(newZoom, minZoom, maxZoom);
transformZoomTo(zoomTarget, focusPoint, ZoomDuration, ZoomEasing); focusPoint ??= zoomedContent.ToLocalSpace(ToScreenSpace(new Vector2(DrawWidth / 2, 0))).X;
transformZoomTo(zoomTarget, focusPoint.Value, ZoomDuration, ZoomEasing);
OnZoomChanged(); OnZoomChanged();
} }
@ -183,7 +196,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private readonly float scrollOffset; private readonly float scrollOffset;
/// <summary> /// <summary>
/// Transforms <see cref="ZoomableScrollContainer.currentZoom"/> to a new value. /// Transforms <see cref="ZoomableScrollContainer.CurrentZoom"/> to a new value.
/// </summary> /// </summary>
/// <param name="focusPoint">The focus point in absolute coordinates local to the content.</param> /// <param name="focusPoint">The focus point in absolute coordinates local to the content.</param>
/// <param name="contentSize">The size of the content.</param> /// <param name="contentSize">The size of the content.</param>
@ -195,7 +208,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
this.scrollOffset = scrollOffset; this.scrollOffset = scrollOffset;
} }
public override string TargetMember => nameof(currentZoom); public override string TargetMember => nameof(CurrentZoom);
private float valueAt(double time) private float valueAt(double time)
{ {
@ -213,7 +226,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
float expectedWidth = d.DrawWidth * newZoom; float expectedWidth = d.DrawWidth * newZoom;
float targetOffset = expectedWidth * (focusPoint / contentSize) - focusOffset; float targetOffset = expectedWidth * (focusPoint / contentSize) - focusOffset;
d.currentZoom = newZoom; d.CurrentZoom = newZoom;
d.updateZoomedContentWidth(); d.updateZoomedContentWidth();
// Temporarily here to make sure ScrollTo gets the correct DrawSize for scrollable area. // Temporarily here to make sure ScrollTo gets the correct DrawSize for scrollable area.
@ -222,7 +235,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
d.ScrollTo(targetOffset, false); d.ScrollTo(targetOffset, false);
} }
protected override void ReadIntoStartValue(ZoomableScrollContainer d) => StartValue = d.currentZoom; protected override void ReadIntoStartValue(ZoomableScrollContainer d) => StartValue = d.CurrentZoom;
} }
} }
} }

View File

@ -70,6 +70,7 @@ namespace osu.Game.Screens.Play
{ {
ScoreInfo = ScoreInfo =
{ {
BeatmapInfo = beatmap.BeatmapInfo,
Ruleset = ruleset.RulesetInfo Ruleset = ruleset.RulesetInfo
} }
}; };

View File

@ -267,12 +267,7 @@ namespace osu.Game.Screens.Play
}, },
FailOverlay = new FailOverlay FailOverlay = new FailOverlay
{ {
SaveReplay = () => SaveReplay = prepareAndImportScore,
{
Score.ScoreInfo.Passed = false;
Score.ScoreInfo.Rank = ScoreRank.F;
return prepareAndImportScore();
},
OnRetry = Restart, OnRetry = Restart,
OnQuit = () => PerformExit(true), OnQuit = () => PerformExit(true),
}, },
@ -831,7 +826,6 @@ namespace osu.Game.Screens.Play
return false; return false;
GameplayState.HasFailed = true; GameplayState.HasFailed = true;
Score.ScoreInfo.Passed = false;
updateGameplayState(); updateGameplayState();
@ -849,9 +843,16 @@ namespace osu.Game.Screens.Play
return true; return true;
} }
// Called back when the transform finishes /// <summary>
/// Invoked when the fail animation has finished.
/// </summary>
private void onFailComplete() private void onFailComplete()
{ {
// fail completion is a good point to mark a score as failed,
// since the last judgement that caused the fail only applies to score processor after onFail.
// todo: this should probably be handled better.
ScoreProcessor.FailScore(Score.ScoreInfo);
GameplayClockContainer.Stop(); GameplayClockContainer.Stop();
FailOverlay.Retries = RestartCount; FailOverlay.Retries = RestartCount;
@ -1028,10 +1029,7 @@ namespace osu.Game.Screens.Play
// if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap. // if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap.
if (prepareScoreForDisplayTask == null) if (prepareScoreForDisplayTask == null)
{ ScoreProcessor.FailScore(Score.ScoreInfo);
Score.ScoreInfo.Passed = false;
Score.ScoreInfo.Rank = ScoreRank.F;
}
// EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous. // EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous.
// To resolve test failures, forcefully end playing synchronously when this screen exits. // To resolve test failures, forcefully end playing synchronously when this screen exits.

View File

@ -133,7 +133,7 @@ namespace osu.Game.Screens.Ranking.Expanded
FillMode = FillMode.Fit, FillMode = FillMode.Fit,
} }
}, },
scoreCounter = new TotalScoreCounter scoreCounter = new TotalScoreCounter(!withFlair)
{ {
Margin = new MarginPadding { Top = 0, Bottom = 5 }, Margin = new MarginPadding { Top = 0, Bottom = 5 },
Current = { Value = 0 }, Current = { Value = 0 },

View File

@ -4,6 +4,8 @@
#nullable disable #nullable disable
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -21,13 +23,19 @@ namespace osu.Game.Screens.Ranking.Expanded
{ {
private readonly APIUser user; private readonly APIUser user;
private Sample appearanceSample;
private readonly bool playAppearanceSound;
/// <summary> /// <summary>
/// Creates a new <see cref="ExpandedPanelTopContent"/>. /// Creates a new <see cref="ExpandedPanelTopContent"/>.
/// </summary> /// </summary>
/// <param name="user">The <see cref="APIUser"/> to display.</param> /// <param name="user">The <see cref="APIUser"/> to display.</param>
public ExpandedPanelTopContent(APIUser user) /// <param name="playAppearanceSound">Whether the appearance sample should play</param>
public ExpandedPanelTopContent(APIUser user, bool playAppearanceSound = false)
{ {
this.user = user; this.user = user;
this.playAppearanceSound = playAppearanceSound;
Anchor = Anchor.TopCentre; Anchor = Anchor.TopCentre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
@ -35,8 +43,10 @@ namespace osu.Game.Screens.Ranking.Expanded
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(AudioManager audio)
{ {
appearanceSample = audio.Samples.Get(@"Results/score-panel-top-appear");
InternalChild = new FillFlowContainer InternalChild = new FillFlowContainer
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
@ -62,5 +72,13 @@ namespace osu.Game.Screens.Ranking.Expanded
} }
}; };
} }
protected override void LoadComplete()
{
base.LoadComplete();
if (playAppearanceSound)
appearanceSample?.Play();
}
} }
} }

View File

@ -3,7 +3,11 @@
#nullable disable #nullable disable
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -22,11 +26,35 @@ namespace osu.Game.Screens.Ranking.Expanded
protected override Easing RollingEasing => AccuracyCircle.ACCURACY_TRANSFORM_EASING; protected override Easing RollingEasing => AccuracyCircle.ACCURACY_TRANSFORM_EASING;
public TotalScoreCounter() private readonly bool playSamples;
private readonly Bindable<double> tickPlaybackRate = new Bindable<double>();
private double lastSampleTime;
private DrawableSample sampleTick;
public TotalScoreCounter(bool playSamples = false)
{ {
// Todo: AutoSize X removed here due to https://github.com/ppy/osu-framework/issues/3369 // Todo: AutoSize X removed here due to https://github.com/ppy/osu-framework/issues/3369
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
this.playSamples = playSamples;
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
AddInternal(sampleTick = new DrawableSample(audio.Samples.Get(@"Results/score-tick-lesser")));
}
protected override void LoadComplete()
{
base.LoadComplete();
if (playSamples)
Current.BindValueChanged(_ => startTicking());
} }
protected override LocalisableString FormatCount(long count) => count.ToString("N0"); protected override LocalisableString FormatCount(long count) => count.ToString("N0");
@ -39,5 +67,35 @@ namespace osu.Game.Screens.Ranking.Expanded
s.Font = OsuFont.Torus.With(size: 60, weight: FontWeight.Light, fixedWidth: true); s.Font = OsuFont.Torus.With(size: 60, weight: FontWeight.Light, fixedWidth: true);
s.Spacing = new Vector2(-5, 0); s.Spacing = new Vector2(-5, 0);
}); });
public override long DisplayedCount
{
get => base.DisplayedCount;
set
{
if (base.DisplayedCount == value)
return;
base.DisplayedCount = value;
if (playSamples && Time.Current > lastSampleTime + tickPlaybackRate.Value)
{
sampleTick?.Play();
lastSampleTime = Time.Current;
}
}
}
private void startTicking()
{
const double tick_debounce_rate_start = 10f;
const double tick_debounce_rate_end = 100f;
const double tick_volume_start = 0.5f;
const double tick_volume_end = 1.0f;
this.TransformBindableTo(tickPlaybackRate, tick_debounce_rate_start);
this.TransformBindableTo(tickPlaybackRate, tick_debounce_rate_end, RollingDuration, Easing.OutSine);
sampleTick.VolumeTo(tick_volume_start).Then().VolumeTo(tick_volume_end, RollingDuration, Easing.OutSine);
}
} }
} }

View File

@ -7,6 +7,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -60,6 +62,8 @@ namespace osu.Game.Screens.Ranking
private readonly bool allowRetry; private readonly bool allowRetry;
private readonly bool allowWatchingReplay; private readonly bool allowWatchingReplay;
private Sample popInSample;
protected ResultsScreen(ScoreInfo score, bool allowRetry, bool allowWatchingReplay = true) protected ResultsScreen(ScoreInfo score, bool allowRetry, bool allowWatchingReplay = true)
{ {
Score = score; Score = score;
@ -70,10 +74,12 @@ namespace osu.Game.Screens.Ranking
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(AudioManager audio)
{ {
FillFlowContainer buttons; FillFlowContainer buttons;
popInSample = audio.Samples.Get(@"UI/overlay-pop-in");
InternalChild = new GridContainer InternalChild = new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -244,6 +250,8 @@ namespace osu.Game.Screens.Ranking
}); });
bottomPanel.FadeTo(1, 250); bottomPanel.FadeTo(1, 250);
popInSample?.Play();
} }
public override bool OnExiting(ScreenExitEvent e) public override bool OnExiting(ScreenExitEvent e)

View File

@ -6,13 +6,16 @@
using System; using System;
using osu.Framework; using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Contracted; using osu.Game.Screens.Ranking.Contracted;
using osu.Game.Screens.Ranking.Expanded; using osu.Game.Screens.Ranking.Expanded;
@ -93,9 +96,12 @@ namespace osu.Game.Screens.Ranking
public readonly ScoreInfo Score; public readonly ScoreInfo Score;
private bool displayWithFlair; [Resolved]
private OsuGameBase game { get; set; }
private Container content; private DrawableAudioMixer mixer;
private bool displayWithFlair;
private Container topLayerContainer; private Container topLayerContainer;
private Drawable topLayerBackground; private Drawable topLayerBackground;
@ -107,6 +113,8 @@ namespace osu.Game.Screens.Ranking
private Container middleLayerContentContainer; private Container middleLayerContentContainer;
private Drawable middleLayerContent; private Drawable middleLayerContent;
private DrawableSample samplePanelFocus;
public ScorePanel(ScoreInfo score, bool isNewLocalScore = false) public ScorePanel(ScoreInfo score, bool isNewLocalScore = false)
{ {
Score = score; Score = score;
@ -116,13 +124,13 @@ namespace osu.Game.Screens.Ranking
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(AudioManager audio)
{ {
// ScorePanel doesn't include the top extruding area in its own size. // ScorePanel doesn't include the top extruding area in its own size.
// Adding a manual offset here allows the expanded version to take on an "acceptable" vertical centre when at 100% UI scale. // Adding a manual offset here allows the expanded version to take on an "acceptable" vertical centre when at 100% UI scale.
const float vertical_fudge = 20; const float vertical_fudge = 20;
InternalChild = content = new Container InternalChild = mixer = new DrawableAudioMixer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -174,7 +182,8 @@ namespace osu.Game.Screens.Ranking
}, },
middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both } middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both }
} }
} },
samplePanelFocus = new DrawableSample(audio.Samples.Get(@"Results/score-panel-focus"))
} }
}; };
} }
@ -202,12 +211,32 @@ namespace osu.Game.Screens.Ranking
state = value; state = value;
if (IsLoaded) if (IsLoaded)
{
updateState(); updateState();
if (value == PanelState.Expanded)
playAppearSample();
}
StateChanged?.Invoke(value); StateChanged?.Invoke(value);
} }
} }
protected override void Update()
{
base.Update();
mixer.Balance.Value = (ScreenSpaceDrawQuad.Centre.X / game.ScreenSpaceDrawQuad.Width) * 2 - 1;
}
private void playAppearSample()
{
var channel = samplePanelFocus?.GetChannel();
if (channel == null) return;
channel.Frequency.Value = 0.99 + RNG.NextDouble(0.2);
channel.Play();
}
private void updateState() private void updateState()
{ {
topLayerContent?.FadeOut(content_fade_duration).Expire(); topLayerContent?.FadeOut(content_fade_duration).Expire();
@ -221,7 +250,8 @@ namespace osu.Game.Screens.Ranking
topLayerBackground.FadeColour(expanded_top_layer_colour, RESIZE_DURATION, Easing.OutQuint); topLayerBackground.FadeColour(expanded_top_layer_colour, RESIZE_DURATION, Easing.OutQuint);
middleLayerBackground.FadeColour(expanded_middle_layer_colour, RESIZE_DURATION, Easing.OutQuint); middleLayerBackground.FadeColour(expanded_middle_layer_colour, RESIZE_DURATION, Easing.OutQuint);
topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User) { Alpha = 0 }); bool firstLoad = topLayerContent == null;
topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User, firstLoad) { Alpha = 0 });
middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, displayWithFlair) { Alpha = 0 }); middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, displayWithFlair) { Alpha = 0 });
// only the first expanded display should happen with flair. // only the first expanded display should happen with flair.
@ -244,7 +274,7 @@ namespace osu.Game.Screens.Ranking
break; break;
} }
content.ResizeTo(Size, RESIZE_DURATION, Easing.OutQuint); mixer.ResizeTo(Size, RESIZE_DURATION, Easing.OutQuint);
bool topLayerExpanded = topLayerContainer.Y < 0; bool topLayerExpanded = topLayerContainer.Y < 0;

View File

@ -8,7 +8,10 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
@ -35,6 +38,10 @@ namespace osu.Game.Screens.Ranking.Statistics
private readonly Container content; private readonly Container content;
private readonly LoadingSpinner spinner; private readonly LoadingSpinner spinner;
private bool wasOpened;
private Sample popInSample;
private Sample popOutSample;
public StatisticsPanel() public StatisticsPanel()
{ {
InternalChild = new Container InternalChild = new Container
@ -56,9 +63,12 @@ namespace osu.Game.Screens.Ranking.Statistics
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(AudioManager audio)
{ {
Score.BindValueChanged(populateStatistics, true); Score.BindValueChanged(populateStatistics, true);
popInSample = audio.Samples.Get(@"Results/statistics-panel-pop-in");
popOutSample = audio.Samples.Get(@"Results/statistics-panel-pop-out");
} }
private CancellationTokenSource loadCancellation; private CancellationTokenSource loadCancellation;
@ -81,18 +91,16 @@ namespace osu.Game.Screens.Ranking.Statistics
spinner.Show(); spinner.Show();
var localCancellationSource = loadCancellation = new CancellationTokenSource(); var localCancellationSource = loadCancellation = new CancellationTokenSource();
IBeatmap playableBeatmap = null;
var workingBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo);
// Todo: The placement of this is temporary. Eventually we'll both generate the playable beatmap _and_ run through it in a background task to generate the hit events. // Todo: The placement of this is temporary. Eventually we'll both generate the playable beatmap _and_ run through it in a background task to generate the hit events.
Task.Run(() => Task.Run(() => workingBeatmap.GetPlayableBeatmap(newScore.Ruleset, newScore.Mods), loadCancellation.Token).ContinueWith(task => Schedule(() =>
{
playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods);
}, loadCancellation.Token).ContinueWith(_ => Schedule(() =>
{ {
bool hitEventsAvailable = newScore.HitEvents.Count != 0; bool hitEventsAvailable = newScore.HitEvents.Count != 0;
Container<Drawable> container; Container<Drawable> container;
var statisticRows = newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap); var statisticRows = newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, task.GetResultSafely());
if (!hitEventsAvailable && statisticRows.SelectMany(r => r.Columns).All(c => c.RequiresHitEvents)) if (!hitEventsAvailable && statisticRows.SelectMany(r => r.Columns).All(c => c.RequiresHitEvents))
{ {
@ -216,9 +224,21 @@ namespace osu.Game.Screens.Ranking.Statistics
return true; return true;
} }
protected override void PopIn() => this.FadeIn(150, Easing.OutQuint); protected override void PopIn()
{
this.FadeIn(150, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(150, Easing.OutQuint); popInSample?.Play();
wasOpened = true;
}
protected override void PopOut()
{
this.FadeOut(150, Easing.OutQuint);
if (wasOpened)
popOutSample?.Play();
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {

View File

@ -115,42 +115,53 @@ namespace osu.Game.Screens.Select
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
}, },
new FillFlowContainer new GridContainer
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Direction = FillDirection.Horizontal,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Spacing = new Vector2(OsuTabControl<SortMode>.HORIZONTAL_SPACING, 0), ColumnDimensions = new[]
Children = new Drawable[]
{ {
new OsuTabControlCheckbox new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, OsuTabControl<SortMode>.HORIZONTAL_SPACING),
new Dimension(),
new Dimension(GridSizeMode.Absolute, OsuTabControl<SortMode>.HORIZONTAL_SPACING),
new Dimension(GridSizeMode.AutoSize),
},
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
Content = new[]
{
new[]
{ {
Text = "Show converted", new OsuSpriteText
Current = config.GetBindable<bool>(OsuSetting.ShowConvertedBeatmaps), {
Anchor = Anchor.BottomRight, Text = SortStrings.Default,
Origin = Anchor.BottomRight, Font = OsuFont.GetFont(size: 14),
}, Margin = new MarginPadding(5),
sortTabs = new OsuTabControl<SortMode> Anchor = Anchor.BottomRight,
{ Origin = Anchor.BottomRight,
RelativeSizeAxes = Axes.X, },
Width = 0.5f, Empty(),
Height = 24, sortTabs = new OsuTabControl<SortMode>
AutoSort = true, {
Anchor = Anchor.BottomRight, RelativeSizeAxes = Axes.X,
Origin = Anchor.BottomRight, Height = 24,
AccentColour = colours.GreenLight, AutoSort = true,
Current = { BindTarget = sortMode } Anchor = Anchor.BottomRight,
}, Origin = Anchor.BottomRight,
new OsuSpriteText AccentColour = colours.GreenLight,
{ Current = { BindTarget = sortMode }
Text = SortStrings.Default, },
Font = OsuFont.GetFont(size: 14), Empty(),
Margin = new MarginPadding(5), new OsuTabControlCheckbox
Anchor = Anchor.BottomRight, {
Origin = Anchor.BottomRight, Text = "Show converted",
}, Current = config.GetBindable<bool>(OsuSetting.ShowConvertedBeatmaps),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
}
} }
}, },
} }

View File

@ -36,7 +36,7 @@ namespace osu.Game.Screens.Utility
public readonly Bindable<LatencyVisualMode> VisualMode = new Bindable<LatencyVisualMode>(); public readonly Bindable<LatencyVisualMode> VisualMode = new Bindable<LatencyVisualMode>();
public CursorContainer? Cursor { get; private set; } public CursorContainer? MenuCursor { get; private set; }
public bool ProvidingUserCursor => IsActiveArea.Value; public bool ProvidingUserCursor => IsActiveArea.Value;
@ -91,7 +91,7 @@ namespace osu.Game.Screens.Utility
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
Cursor = new LatencyCursorContainer MenuCursor = new LatencyCursorContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
@ -105,7 +105,7 @@ namespace osu.Game.Screens.Utility
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
Cursor = new LatencyCursorContainer MenuCursor = new LatencyCursorContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
@ -119,7 +119,7 @@ namespace osu.Game.Screens.Utility
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
Cursor = new LatencyCursorContainer MenuCursor = new LatencyCursorContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },

View File

@ -171,6 +171,11 @@ namespace osu.Game.Tests.Visual
API.Login("Rhythm Champion", "osu!"); API.Login("Rhythm Champion", "osu!");
Dependencies.Get<SessionStatics>().SetValue(Static.MutedAudioNotificationShownOnce, true); Dependencies.Get<SessionStatics>().SetValue(Static.MutedAudioNotificationShownOnce, true);
// set applied version to latest so that the BackgroundBeatmapProcessor doesn't consider
// beatmap star ratings as outdated and reset them throughout the test.
foreach (var ruleset in RulesetStore.AvailableRulesets)
ruleset.LastAppliedDifficultyVersion = ruleset.CreateInstance().CreateDifficultyCalculator(Beatmap.Default).Version;
} }
protected override void Update() protected override void Update()

View File

@ -38,11 +38,11 @@ namespace osu.Game.Tests.Visual
protected OsuManualInputManagerTestScene() protected OsuManualInputManagerTestScene()
{ {
MenuCursorContainer cursorContainer; GlobalCursorDisplay cursorDisplay;
CompositeDrawable mainContent = cursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }; CompositeDrawable mainContent = cursorDisplay = new GlobalCursorDisplay { RelativeSizeAxes = Axes.Both };
cursorContainer.Child = content = new OsuTooltipContainer(cursorContainer.Cursor) cursorDisplay.Child = content = new OsuTooltipContainer(cursorDisplay.MenuCursor)
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}; };

View File

@ -36,8 +36,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.14.0" /> <PackageReference Include="Realm" Version="10.14.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.720.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.722.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.716.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.722.0" />
<PackageReference Include="Sentry" Version="3.19.0" /> <PackageReference Include="Sentry" Version="3.19.0" />
<PackageReference Include="SharpCompress" Version="0.32.1" /> <PackageReference Include="SharpCompress" Version="0.32.1" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />

View File

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