1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-28 07:23:14 +08:00

Merge branch 'ppy:master' into add-missing-icons-to-recent-profile-section

This commit is contained in:
LukynkaCZE 2022-07-25 14:56:00 +02:00 committed by GitHub
commit a7c92fe5d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 2439 additions and 769 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -23,13 +23,29 @@ namespace osu.Game.Rulesets.Taiko.Tests
protected DrawableTaikoRuleset DrawableRuleset { get; private set; }
protected Container PlayfieldContainer { get; private set; }
private ControlPointInfo controlPointInfo { get; set; }
[BackgroundDependencyLoader]
private void load()
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo = new ControlPointInfo();
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 } },
BeatmapInfo = new BeatmapInfo
@ -41,19 +57,10 @@ namespace osu.Game.Rulesets.Taiko.Tests
Title = @"Sample Beatmap",
Author = { Username = @"peppy" },
},
Ruleset = new TaikoRuleset().RulesetInfo
Ruleset = ruleset
},
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,
Origin = Anchor.Centre,
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 stamina_skill_multiplier = 0.021;
public override int Version => 20220701;
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
{

View File

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

View File

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

View File

@ -8,18 +8,18 @@ using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
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.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Input.Handlers;
using osu.Game.Replays;
using osu.Game.Rulesets.Mods;
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.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Scoring;
using osu.Game.Skinning;
@ -56,6 +56,8 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.X,
Depth = float.MaxValue
});
KeyBindingInputManager.Add(new DrumTouchInputArea());
}
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.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Ranking;
using osu.Game.Skinning;
using osuTK;
@ -27,13 +25,8 @@ namespace osu.Game.Rulesets.Taiko.UI
{
private const float middle_split = 0.025f;
[Cached]
private DrumSampleTriggerSource sampleTriggerSource;
public InputDrum(HitObjectContainer hitObjectContainer)
public InputDrum()
{
sampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer);
AutoSizeAxes = Axes.X;
RelativeSizeAxes = Axes.Y;
}
@ -48,7 +41,6 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
},
sampleTriggerSource
};
}
@ -116,9 +108,6 @@ namespace osu.Game.Rulesets.Taiko.UI
private readonly Sprite centre;
private readonly Sprite centreHit;
[Resolved]
private DrumSampleTriggerSource sampleTriggerSource { get; set; }
public TaikoHalfDrum(bool flipped)
{
Masking = true;
@ -179,15 +168,11 @@ namespace osu.Game.Rulesets.Taiko.UI
{
target = centreHit;
back = centre;
sampleTriggerSource.Play(HitType.Centre);
}
else if (e.Action == RimAction)
{
target = rimHit;
back = rim;
sampleTriggerSource.Play(HitType.Rim);
}
if (target != null)

View File

@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
inputDrum = new InputDrum(HitObjectContainer)
inputDrum = new InputDrum
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
@ -164,6 +164,7 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.Both,
},
drumRollHitContainer.CreateProxy(),
new DrumSamplePlayer(HitObjectContainer),
// 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.
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

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@ -15,6 +16,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Gameplay
@ -91,6 +93,47 @@ namespace osu.Game.Tests.Gameplay
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
{
public override HitResult MaxResult { get; }
@ -100,5 +143,17 @@ namespace osu.Game.Tests.Gameplay
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

@ -5,10 +5,10 @@
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Utils;
namespace osu.Game.Tests.Visual.Editing
{
[Ignore("Timeline initialisation is kinda broken.")] // Initial work to rectify this was done in https://github.com/ppy/osu/pull/19297, but needs more massaging to work.
public class TestSceneTimelineZoom : TimelineTestScene
{
public override Drawable CreateTestComponent() => Empty();
@ -32,12 +32,12 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange);
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);
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);
AddAssert("range restored", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange, 1));
AddStep("range restored", () => Assert.That(TimelineArea.Timeline.VisibleRange, Is.EqualTo(initialVisibleRange).Within(1)));
}
[Test]

View File

@ -3,10 +3,10 @@
#nullable disable
using System;
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.Framework.Graphics.Shapes;
@ -40,8 +40,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private TestReplayRecorder recorder;
[Cached]
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
private GameplayState gameplayState;
[SetUpSteps]
public void SetUpSteps()
@ -52,81 +51,15 @@ namespace osu.Game.Tests.Visual.Gameplay
{
replay = new Replay();
Add(new GridContainer
gameplayState = TestGameplayState.Create(new OsuRuleset());
gameplayState.Score.Replay = replay;
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
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()
}
},
}
}
}
});
CachedDependencies = new (Type, object)[] { (typeof(GameplayState), gameplayState) },
Child = createContent(),
};
});
}
@ -203,6 +136,74 @@ namespace osu.Game.Tests.Visual.Gameplay
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 TestFramedReplayInputHandler(Replay replay)

View File

@ -5,6 +5,7 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
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);
}
[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()
{
base.TearDownSteps();

View File

@ -24,7 +24,6 @@ using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mods;
@ -633,7 +632,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
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);

View File

@ -16,6 +16,10 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Online.API.Requests.Responses;
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;
namespace osu.Game.Tests.Visual.Online
@ -34,6 +38,9 @@ namespace osu.Game.Tests.Visual.Online
[Resolved]
private IRulesetStore rulesets { get; set; }
[SetUp]
public void SetUp() => Schedule(() => SelectedMods.Value = Array.Empty<Mod>());
[Test]
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]
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);
}
[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 APIScoresCollection createScores()

View File

@ -7,6 +7,7 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
@ -99,6 +100,23 @@ namespace osu.Game.Tests.Visual.Online
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
{
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(secondScore)),
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(secondScore, 0.85)),
new ColourProvidedContainer(OverlayColourScheme.Pink, new DrawableProfileWeightedScore(thirdScore, 0.66)),

View File

@ -8,8 +8,10 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
@ -17,11 +19,26 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneLabelledSliderBar : OsuTestScene
{
[TestCase(false)]
[TestCase(true)]
public void TestSliderBar(bool hasDescription) => createSliderBar(hasDescription);
[Test]
public void TestBasic() => createSliderBar();
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", () =>
{
@ -38,6 +55,8 @@ namespace osu.Game.Tests.Visual.UserInterface
{
new LabelledSliderBar<double>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Current = new BindableDouble(5)
{
MinValue = 0,
@ -45,7 +64,6 @@ namespace osu.Game.Tests.Visual.UserInterface
Precision = 1,
},
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)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = new LabelledSliderBar<double>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Current = new BindableDouble(5)
{
MinValue = 0,
@ -65,7 +87,6 @@ namespace osu.Game.Tests.Visual.UserInterface
Precision = 1,
},
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,
Spacing = new Vector2(2f),
Direction = FillDirection.Horizontal,
ChildrenEnumerable = Enumerable.Range(0, 15).Select(i => new FillFlowContainer
ChildrenEnumerable = Enumerable.Range(-1, 15).Select(i => new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

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 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]
public string MD5Hash { get; set; } = string.Empty;

View File

@ -439,12 +439,15 @@ namespace osu.Game.Beatmaps
{
if (beatmapInfo != null)
{
// Detached sets don't come with files.
// If we seem to be missing files, now is a good time to re-fetch.
if (refetch || beatmapInfo.IsManaged || beatmapInfo.BeatmapSet?.Files.Count == 0)
{
if (refetch)
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;
beatmapInfo = Realm.Run(r => r.Find<BeatmapInfo>(id)?.Detach()) ?? beatmapInfo;
}

View File

@ -151,7 +151,7 @@ namespace osu.Game.Beatmaps.Drawables
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);

View File

@ -63,8 +63,9 @@ namespace osu.Game.Database
/// 17 2022-07-16 Added CountryCode to RealmUser.
/// 18 2022-07-19 Added OnlineMD5Hash and LastOnlineUpdate to BeatmapInfo.
/// 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>
private const int schema_version = 19;
private const int schema_version = 20;
/// <summary>
/// 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:
foreach (var beatmap in migration.NewRealm.All<BeatmapInfo>())
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;
}
}

View File

@ -10,9 +10,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics.Sprites;
@ -22,8 +21,8 @@ namespace osu.Game.Graphics.UserInterface
{
public class FPSCounter : VisibilityContainer, IHasCustomTooltip
{
private RollingCounter<double> counterUpdateFrameTime = null!;
private RollingCounter<double> counterDrawFPS = null!;
private OsuSpriteText counterUpdateFrameTime = null!;
private OsuSpriteText counterDrawFPS = null!;
private Container mainContent = null!;
@ -31,10 +30,32 @@ namespace osu.Game.Graphics.UserInterface
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 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]
private OsuColour colours { get; set; } = null!;
@ -44,7 +65,7 @@ namespace osu.Game.Graphics.UserInterface
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
private void load(OsuConfigManager config, GameHost gameHost)
{
InternalChildren = new Drawable[]
{
@ -77,20 +98,23 @@ namespace osu.Game.Graphics.UserInterface
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
counterUpdateFrameTime = new FrameTimeCounter
counterUpdateFrameTime = new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Margin = new MarginPadding(1),
Font = OsuFont.Default.With(fixedWidth: true, size: 16, weight: FontWeight.SemiBold),
Spacing = new Vector2(-1),
Y = -2,
},
counterDrawFPS = new FramesPerSecondCounter
counterDrawFPS = new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Margin = new MarginPadding(2),
Font = OsuFont.Default.With(fixedWidth: true, size: 13, weight: FontWeight.SemiBold),
Spacing = new Vector2(-2),
Y = 10,
Scale = new Vector2(0.8f),
}
}
},
@ -99,19 +123,23 @@ namespace osu.Game.Graphics.UserInterface
};
config.BindWith(OsuSetting.ShowFpsDisplay, showFpsDisplay);
drawClock = gameHost.DrawThread.Clock;
updateClock = gameHost.UpdateThread.Clock;
inputClock = gameHost.InputThread.Clock;
}
protected override void LoadComplete()
{
base.LoadComplete();
displayTemporarily();
requestDisplay();
showFpsDisplay.BindValueChanged(showFps =>
{
State.Value = showFps.NewValue ? Visibility.Visible : Visibility.Hidden;
if (showFps.NewValue)
displayTemporarily();
requestDisplay();
}, true);
State.BindValueChanged(state => showFpsDisplay.Value = state.NewValue == Visibility.Visible);
@ -124,48 +152,17 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e)
{
background.FadeTo(1, 200);
displayTemporarily();
requestDisplay();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
background.FadeTo(idle_background_alpha, 200);
displayTemporarily();
requestDisplay();
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()
{
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).
bool aimRatesChanged = updateAimFPS();
// TODO: this is wrong (elapsed clock time, not actual run time).
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;
bool hasUpdateSpike = displayedFrameTime < spike_time_ms && updateClock.ElapsedFrameTime > spike_time_ms;
// 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.
if (hasUpdateSpike)
counterUpdateFrameTime.SetCountWithoutRolling(newUpdateFrameTime);
else
counterUpdateFrameTime.Current.Value = newUpdateFrameTime;
// note that we use an elapsed time here of 1 intentionally.
// this weights all updates equally. if we passed in the elapsed time, longer frames would be weighted incorrectly lower.
displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, updateClock.ElapsedFrameTime, hasUpdateSpike ? 0 : 100, 1);
if (hasDrawSpike)
// 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
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;
counterUpdateFrameTime.Colour = getColour(displayedUpdateFPS / aimUpdateFPS);
lastUpdate = Time.Current;
}
bool hasSignificantChanges = aimRatesChanged
|| hasDrawSpike
|| hasUpdateSpike
|| counterDrawFPS.DisplayedCount < aimDrawFPS * 0.8
|| displayedUpdateFPS < aimUpdateFPS * 0.8;
|| displayedFpsCount < aimDrawFPS * 0.8
|| 1000 / displayedFrameTime < aimUpdateFPS * 0.8;
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()
{
if (gameHost.UpdateThread.Clock.Throttling)
if (updateClock.Throttling)
{
double newAimDrawFPS = gameHost.DrawThread.Clock.MaximumUpdateHz;
double newAimUpdateFPS = gameHost.UpdateThread.Clock.MaximumUpdateHz;
double newAimDrawFPS = drawClock.MaximumUpdateHz;
double newAimUpdateFPS = updateClock.MaximumUpdateHz;
if (aimDrawFPS != newAimDrawFPS || aimUpdateFPS != newAimUpdateFPS)
{
@ -230,7 +252,7 @@ namespace osu.Game.Graphics.UserInterface
}
else
{
double newAimFPS = gameHost.InputThread.Clock.MaximumUpdateHz;
double newAimFPS = inputClock.MaximumUpdateHz;
if (aimDrawFPS != newAimFPS || aimUpdateFPS != newAimFPS)
{
@ -253,50 +275,5 @@ namespace osu.Game.Graphics.UserInterface
public ITooltip GetCustomTooltip() => new FPSCounterTooltip();
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()
{
base.UpdateAfterChildren();
LeftBox.Scale = new Vector2(Math.Clamp(
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, DrawWidth), 1);
LeftBox.Scale = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2, 0, Math.Max(0, DrawWidth)), 1);
RightBox.Scale = new Vector2(Math.Clamp(DrawWidth - Nub.DrawPosition.X - RangePadding - Nub.DrawWidth / 2, 0, Math.Max(0, DrawWidth)), 1);
}
protected override void UpdateValue(float value)

View File

@ -24,6 +24,11 @@ namespace osu.Game.Localisation
/// </summary>
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}";
}
}

View File

@ -206,6 +206,11 @@ namespace osu.Game.Online.Spectator
if (!IsPlaying)
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)
purgePendingFrames();

View File

@ -904,6 +904,8 @@ namespace osu.Game
loadComponentSingleFile(CreateHighPerformanceSession(), Add);
loadComponentSingleFile(new BackgroundBeatmapProcessor(), Add);
chatOverlay.State.BindValueChanged(_ => updateChatPollRate());
// Multiplayer modes need to increase poll rate temporarily.
API.Activity.BindValueChanged(_ => updateChatPollRate(), true);

View File

@ -280,8 +280,7 @@ namespace osu.Game
AddInternal(difficultyCache);
// 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(multiplayerClient = new OnlineMultiplayerClient(endpoints));
dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));

View File

@ -6,7 +6,6 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
@ -25,6 +24,7 @@ using osu.Framework.Localisation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics.Cursor;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring.Drawables;
namespace osu.Game.Overlays.BeatmapSet.Scores
{
@ -179,8 +179,10 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
if (showPerformancePoints)
{
Debug.Assert(score.PP != null);
content.Add(new StatisticText(score.PP.Value, format: @"N0"));
if (score.PP != null)
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)
@ -222,19 +224,19 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
private class StatisticText : OsuSpriteText, IHasTooltip
{
private readonly double count;
private readonly double? count;
private readonly double? maxCount;
private readonly bool showTooltip;
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.maxCount = maxCount;
this.showTooltip = showTooltip;
Text = count.ToLocalisableString(format);
Text = count?.ToLocalisableString(format) ?? default;
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.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Scoring.Drawables;
using osuTK;
namespace osu.Game.Overlays.BeatmapSet.Scores
@ -121,7 +124,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
maxComboColumn.Text = value.MaxCombo.ToLocalisableString(@"0\x");
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);
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;
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;
}
private readonly OsuTextFlowContainer text;
public LocalisableString Text
{
set => text.Text = value;
}
public Drawable Drawable
{
set
{
text.Clear();
text.AddArbitraryDrawable(value);
}
}
private Bindable<string> current;
public Bindable<string> Current
{
get => text.Current;
set => text.Current = value;
get => current;
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
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
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.Scores;
using osu.Game.Overlays.Comments;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select.Details;
using osuTK;
using osuTK.Graphics;
@ -25,6 +30,14 @@ namespace osu.Game.Overlays
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()
: base(OverlayColourScheme.Blue)
{

View File

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

View File

@ -12,14 +12,10 @@ using osu.Framework.Allocation;
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.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays.Mods.Input;
@ -29,10 +25,8 @@ using osuTK.Graphics;
namespace osu.Game.Overlays.Mods
{
public class ModColumn : CompositeDrawable
public class ModColumn : ModSelectColumn
{
public readonly Container TopLevelContent;
public readonly ModType ModType;
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);
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 Colour4 accentColour;
private Bindable<ModSelectHotkeyStyle> hotkeyStyle = null!;
private IModHotkeyHandler hotkeyHandler = null!;
private Task? latestLoadTask;
internal bool ItemsLoaded => latestLoadTask == null;
private const float header_height = 42;
public ModColumn(ModType modType, bool allowIncompatibleSelection)
{
ModType = modType;
this.allowIncompatibleSelection = allowIncompatibleSelection;
Width = 320;
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();
HeaderText = ModType.Humanize(LetterCasing.Title);
if (allowIncompatibleSelection)
{
controlContainer.Height = 35;
controlContainer.Add(toggleAllCheckbox = new ToggleAllCheckbox(this)
ControlContainer.Height = 35;
ControlContainer.Add(toggleAllCheckbox = new ToggleAllCheckbox(this)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
@ -212,7 +86,7 @@ namespace osu.Game.Overlays.Mods
RelativeSizeAxes = Axes.X,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0)
});
panelFlow.Padding = new MarginPadding
ItemsFlow.Padding = new MarginPadding
{
Top = 0,
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]
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)
{
toggleAllCheckbox.AccentColour = accentColour;
toggleAllCheckbox.AccentHoverColour = accentColour.Lighten(0.3f);
toggleAllCheckbox.AccentColour = AccentColour;
toggleAllCheckbox.AccentHoverColour = AccentColour.Lighten(0.3f);
}
contentContainer.BorderColour = ColourInfo.GradientVertical(colourProvider.Background4, colourProvider.Background3);
contentBackground.Colour = colourProvider.Background4;
hotkeyStyle = configManager.GetBindable<ModSelectHotkeyStyle>(OsuSetting.ModSelectHotkeyStyle);
}
@ -278,7 +136,7 @@ namespace osu.Game.Overlays.Mods
latestLoadTask = loadTask = LoadComponentsAsync(panels, loaded =>
{
panelFlow.ChildrenEnumerable = loaded;
ItemsFlow.ChildrenEnumerable = loaded;
updateState();
}, (cancellationTokenSource = new CancellationTokenSource()).Token);
loadTask.ContinueWith(_ =>

View File

@ -1,144 +1,42 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
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.Input.Events;
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 osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osuTK;
using osuTK.Input;
namespace osu.Game.Overlays.Mods
{
public class ModPanel : OsuClickableContainer
public class ModPanel : ModSelectPanel
{
public Mod Mod => modState.Mod;
public BindableBool Active => modState.Active;
public override BindableBool Active => modState.Active;
public BindableBool Filtered => modState.Filtered;
protected override float IdleSwitchWidth => 54;
protected override float ExpandedSwitchWidth => 70;
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)
{
this.modState = modState;
RelativeSizeAxes = Axes.X;
Height = 42;
Title = Mod.Name;
Description = Mod.Description;
// 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[]
SwitchContainer.Child = new ModSwitchSmall(Mod)
{
Background = new Box
{
RelativeSizeAxes = Axes.Both
},
SwitchContainer = new Container
{
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)
}
}
}
}
}
}
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Active = { BindTarget = Active },
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Scale = new Vector2(HEIGHT / ModSwitchSmall.DEFAULT_SIZE)
};
Action = Active.Toggle;
}
public ModPanel(Mod mod)
@ -146,122 +44,21 @@ namespace osu.Game.Overlays.Mods
{
}
[BackgroundDependencyLoader(true)]
private void load(AudioManager audio, OsuColour colours, ISamplePlaybackDisabler? samplePlaybackDisabler)
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
sampleOn = audio.Samples.Get(@"UI/check-on");
sampleOff = audio.Samples.Get(@"UI/check-off");
activeColour = colours.ForModType(Mod.Type);
if (samplePlaybackDisabler != null)
((IBindable<bool>)samplePlaybackDisabled).BindTo(samplePlaybackDisabler.SamplePlaybackDisabled);
AccentColour = colours.ForModType(Mod.Type);
}
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
protected override void LoadComplete()
{
base.LoadComplete();
Active.BindValueChanged(_ =>
{
playStateChangeSamples();
UpdateState();
});
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
public void ApplyFilter(Func<Mod, bool>? filter)
{
Filtered.Value = filter != null && !filter.Invoke(Mod);
}
private void updateFilterState()
{
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.Rulesets;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring.Drawables;
using osu.Game.Utils;
using osuTK;
@ -218,39 +219,42 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
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,
Direction = FillDirection.Horizontal,
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
}
}
Font = OsuFont.GetFont(weight: FontWeight.Bold),
Text = "-",
Colour = colourProvider.Highlight1
};
}
return new OsuSpriteText
return new FillFlowContainer
{
Font = OsuFont.GetFont(weight: FontWeight.Bold),
Text = "-",
Colour = colourProvider.Highlight1
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
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(),
new Container
{
AutoSizeAxes = Axes.Y,
Width = 50,
Size = new Vector2(50, 14),
Child = new OsuSpriteText
{
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

@ -34,6 +34,11 @@ namespace osu.Game.Rulesets.Difficulty
private readonly IRulesetInfo ruleset;
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)
{
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 JetBrains.Annotations;
using osu.Framework.Testing;
using osu.Game.Rulesets.Difficulty;
using Realms;
namespace osu.Game.Rulesets
@ -22,6 +23,11 @@ namespace osu.Game.Rulesets
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)
{
ShortName = shortName;
@ -86,7 +92,8 @@ namespace osu.Game.Rulesets
Name = Name,
ShortName = ShortName,
InstantiationInfo = InstantiationInfo,
Available = Available
Available = Available,
LastAppliedDifficultyVersion = LastAppliedDifficultyVersion,
};
public Ruleset CreateInstance()

View File

@ -126,6 +126,9 @@ namespace osu.Game.Rulesets.Scoring
private bool beatmapApplied;
private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>();
private Dictionary<HitResult, int>? maximumResultCounts;
private readonly List<HitEvent> hitEvents = new List<HitEvent>();
private HitObject? lastHitObject;
@ -410,12 +413,16 @@ namespace osu.Game.Rulesets.Scoring
{
base.Reset(storeResults);
scoreResultCounts.Clear();
hitEvents.Clear();
lastHitObject = null;
if (storeResults)
{
maximumScoringValues = currentScoringValues;
maximumResultCounts = new Dictionary<HitResult, int>(scoreResultCounts);
}
scoreResultCounts.Clear();
currentScoringValues = default;
currentMaximumScoringValues = default;
@ -423,6 +430,7 @@ namespace osu.Game.Rulesets.Scoring
TotalScore.Value = 0;
Accuracy.Value = 1;
Combo.Value = 0;
Rank.Disabled = false;
Rank.Value = ScoreRank.X;
HighestCombo.Value = 0;
}
@ -445,6 +453,36 @@ namespace osu.Game.Rulesets.Scoring
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)
{
base.ResetFromReplayFrame(frame);

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 osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Threading;
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.
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;
}

View File

@ -41,6 +41,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved]
private EditorClock editorClock { get; set; }
[Resolved]
private EditorBeatmap editorBeatmap { get; set; }
/// <summary>
/// The timeline's scroll position in the last frame.
/// </summary>
@ -68,8 +71,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
/// </summary>
private float defaultTimelineZoom;
private readonly Bindable<double> timelineZoomScale = new BindableDouble(1.0);
public Timeline(Drawable userContent)
{
this.userContent = userContent;
@ -93,7 +94,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private Bindable<float> waveformOpacity;
[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;
@ -154,12 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}, true);
timelineZoomScale.Value = editorBeatmap.BeatmapInfo.TimelineZoom;
timelineZoomScale.BindValueChanged(scale =>
{
Zoom = (float)(defaultTimelineZoom * scale.NewValue);
editorBeatmap.BeatmapInfo.TimelineZoom = scale.NewValue;
}, true);
Zoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom);
}
protected override void LoadComplete()
@ -221,7 +217,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override void OnZoomChanged()
{
base.OnZoomChanged();
timelineZoomScale.Value = Zoom / defaultTimelineZoom;
editorBeatmap.BeatmapInfo.TimelineZoom = Zoom / defaultTimelineZoom;
}
protected override void UpdateAfterChildren()

View File

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

View File

@ -127,7 +127,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (e.AltPressed)
{
// 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;
}
@ -145,12 +145,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
zoomedContentWidthCache.Validate();
}
public void AdjustZoomRelatively(float change, float? focusPoint = null)
{
const float zoom_change_sensitivity = 0.02f;
setZoomTarget(zoomTarget + change * (MaxZoom - minZoom) * zoom_change_sensitivity, focusPoint);
}
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);
transformZoomTo(zoomTarget, focusPoint, ZoomDuration, ZoomEasing);
focusPoint ??= zoomedContent.ToLocalSpace(ToScreenSpace(new Vector2(DrawWidth / 2, 0))).X;
transformZoomTo(zoomTarget, focusPoint.Value, ZoomDuration, ZoomEasing);
OnZoomChanged();
}

View File

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

View File

@ -267,12 +267,7 @@ namespace osu.Game.Screens.Play
},
FailOverlay = new FailOverlay
{
SaveReplay = () =>
{
Score.ScoreInfo.Passed = false;
Score.ScoreInfo.Rank = ScoreRank.F;
return prepareAndImportScore();
},
SaveReplay = prepareAndImportScore,
OnRetry = Restart,
OnQuit = () => PerformExit(true),
},
@ -831,7 +826,6 @@ namespace osu.Game.Screens.Play
return false;
GameplayState.HasFailed = true;
Score.ScoreInfo.Passed = false;
updateGameplayState();
@ -849,9 +843,16 @@ namespace osu.Game.Screens.Play
return true;
}
// Called back when the transform finishes
/// <summary>
/// Invoked when the fail animation has finished.
/// </summary>
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();
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 (prepareScoreForDisplayTask == null)
{
Score.ScoreInfo.Passed = false;
Score.ScoreInfo.Rank = ScoreRank.F;
}
ScoreProcessor.FailScore(Score.ScoreInfo);
// EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous.
// 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,
}
},
scoreCounter = new TotalScoreCounter
scoreCounter = new TotalScoreCounter(!withFlair)
{
Margin = new MarginPadding { Top = 0, Bottom = 5 },
Current = { Value = 0 },

View File

@ -4,6 +4,8 @@
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
@ -21,13 +23,19 @@ namespace osu.Game.Screens.Ranking.Expanded
{
private readonly APIUser user;
private Sample appearanceSample;
private readonly bool playAppearanceSound;
/// <summary>
/// Creates a new <see cref="ExpandedPanelTopContent"/>.
/// </summary>
/// <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.playAppearanceSound = playAppearanceSound;
Anchor = Anchor.TopCentre;
Origin = Anchor.Centre;
@ -35,8 +43,10 @@ namespace osu.Game.Screens.Ranking.Expanded
}
[BackgroundDependencyLoader]
private void load()
private void load(AudioManager audio)
{
appearanceSample = audio.Samples.Get(@"Results/score-panel-top-appear");
InternalChild = new FillFlowContainer
{
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
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@ -22,11 +26,35 @@ namespace osu.Game.Screens.Ranking.Expanded
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
AutoSizeAxes = Axes.Y;
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");
@ -39,5 +67,35 @@ namespace osu.Game.Screens.Ranking.Expanded
s.Font = OsuFont.Torus.With(size: 60, weight: FontWeight.Light, fixedWidth: true);
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.Linq;
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;
@ -60,6 +62,8 @@ namespace osu.Game.Screens.Ranking
private readonly bool allowRetry;
private readonly bool allowWatchingReplay;
private Sample popInSample;
protected ResultsScreen(ScoreInfo score, bool allowRetry, bool allowWatchingReplay = true)
{
Score = score;
@ -70,10 +74,12 @@ namespace osu.Game.Screens.Ranking
}
[BackgroundDependencyLoader]
private void load()
private void load(AudioManager audio)
{
FillFlowContainer buttons;
popInSample = audio.Samples.Get(@"UI/overlay-pop-in");
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.Both,
@ -244,6 +250,8 @@ namespace osu.Game.Screens.Ranking
});
bottomPanel.FadeTo(1, 250);
popInSample?.Play();
}
public override bool OnExiting(ScreenExitEvent e)

View File

@ -6,13 +6,16 @@
using System;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
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.Scoring;
using osu.Game.Screens.Ranking.Contracted;
using osu.Game.Screens.Ranking.Expanded;
@ -93,9 +96,12 @@ namespace osu.Game.Screens.Ranking
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 Drawable topLayerBackground;
@ -107,6 +113,8 @@ namespace osu.Game.Screens.Ranking
private Container middleLayerContentContainer;
private Drawable middleLayerContent;
private DrawableSample samplePanelFocus;
public ScorePanel(ScoreInfo score, bool isNewLocalScore = false)
{
Score = score;
@ -116,13 +124,13 @@ namespace osu.Game.Screens.Ranking
}
[BackgroundDependencyLoader]
private void load()
private void load(AudioManager audio)
{
// 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.
const float vertical_fudge = 20;
InternalChild = content = new Container
InternalChild = mixer = new DrawableAudioMixer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -174,7 +182,8 @@ namespace osu.Game.Screens.Ranking
},
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;
if (IsLoaded)
{
updateState();
if (value == PanelState.Expanded)
playAppearSample();
}
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()
{
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);
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 });
// only the first expanded display should happen with flair.
@ -244,7 +274,7 @@ namespace osu.Game.Screens.Ranking
break;
}
content.ResizeTo(Size, RESIZE_DURATION, Easing.OutQuint);
mixer.ResizeTo(Size, RESIZE_DURATION, Easing.OutQuint);
bool topLayerExpanded = topLayerContainer.Y < 0;

View File

@ -8,6 +8,8 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -35,6 +37,10 @@ namespace osu.Game.Screens.Ranking.Statistics
private readonly Container content;
private readonly LoadingSpinner spinner;
private bool wasOpened;
private Sample popInSample;
private Sample popOutSample;
public StatisticsPanel()
{
InternalChild = new Container
@ -56,9 +62,12 @@ namespace osu.Game.Screens.Ranking.Statistics
}
[BackgroundDependencyLoader]
private void load()
private void load(AudioManager audio)
{
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;
@ -216,9 +225,21 @@ namespace osu.Game.Screens.Ranking.Statistics
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)
{

View File

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

View File

@ -13,6 +13,8 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Testing;
@ -30,7 +32,7 @@ using osu.Game.Screens.Edit.Components.Menus;
namespace osu.Game.Skinning.Editor
{
[Cached(typeof(SkinEditor))]
public class SkinEditor : VisibilityContainer, ICanAcceptFiles
public class SkinEditor : VisibilityContainer, ICanAcceptFiles, IKeyBindingHandler<PlatformAction>
{
public const double TRANSITION_DURATION = 500;
@ -205,6 +207,25 @@ namespace osu.Game.Skinning.Editor
SelectedComponents.BindCollectionChanged((_, _) => Scheduler.AddOnce(populateSettings), true);
}
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
{
switch (e.Action)
{
case PlatformAction.Save:
if (e.Repeat)
return false;
Save();
return true;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)
{
}
public void UpdateTargetScreen(Drawable targetScreen)
{
this.targetScreen = targetScreen;

View File

@ -171,6 +171,11 @@ namespace osu.Game.Tests.Visual
API.Login("Rhythm Champion", "osu!");
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()

View File

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

View File

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