1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-14 17:57:38 +08:00

Merge branch 'ppy:master' into colour-rework

This commit is contained in:
vunyunt 2022-06-06 16:49:08 +08:00 committed by GitHub
commit 29b259ccac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 1356 additions and 447 deletions

View File

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

View File

@ -14,11 +14,11 @@ namespace osu.Game.Rulesets.Mania.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase(2.3449735700206298d, 151, "diffcalc-test")] [TestCase(2.3449735700206298d, 242, "diffcalc-test")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(2.7879104989252959d, 151, "diffcalc-test")] [TestCase(2.7879104989252959d, 242, "diffcalc-test")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime());

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
foreach (var v in base.ToDatabaseAttributes()) foreach (var v in base.ToDatabaseAttributes())
yield return v; yield return v;
// Todo: osu!mania doesn't output MaxCombo attribute for some reason. yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
yield return (ATTRIB_ID_DIFFICULTY, StarRating); yield return (ATTRIB_ID_DIFFICULTY, StarRating);
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
yield return (ATTRIB_ID_SCORE_MULTIPLIER, ScoreMultiplier); yield return (ATTRIB_ID_SCORE_MULTIPLIER, ScoreMultiplier);
@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{ {
base.FromDatabaseAttributes(values); base.FromDatabaseAttributes(values);
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
StarRating = values[ATTRIB_ID_DIFFICULTY]; StarRating = values[ATTRIB_ID_DIFFICULTY];
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
ScoreMultiplier = values[ATTRIB_ID_SCORE_MULTIPLIER]; ScoreMultiplier = values[ATTRIB_ID_SCORE_MULTIPLIER];

View File

@ -52,10 +52,18 @@ namespace osu.Game.Rulesets.Mania.Difficulty
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being. // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate), GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
ScoreMultiplier = getScoreMultiplier(mods), ScoreMultiplier = getScoreMultiplier(mods),
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1), MaxCombo = beatmap.HitObjects.Sum(maxComboForObject)
}; };
} }
private static int maxComboForObject(HitObject hitObject)
{
if (hitObject is HoldNote hold)
return 1 + (int)((hold.EndTime - hold.StartTime) / 100);
return 1;
}
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{ {
var sortedObjects = beatmap.HitObjects.ToArray(); var sortedObjects = beatmap.HitObjects.ToArray();

View File

@ -78,7 +78,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
} }
}); });
if (!(source.FindProvider(s => s.GetTexture("spinner-top") != null) is DefaultLegacySkin)) var topProvider = source.FindProvider(s => s.GetTexture("spinner-top") != null);
if (topProvider is LegacySkinTransformer transformer && !(transformer.Skin is DefaultLegacySkin))
{ {
AddInternal(ApproachCircle = new Sprite AddInternal(ApproachCircle = new Sprite
{ {

View File

@ -710,7 +710,7 @@ namespace osu.Game.Tests.Database
var imported = await LoadOszIntoStore(importer, realm.Realm); var imported = await LoadOszIntoStore(importer, realm.Realm);
realm.Realm.Write(() => await realm.Realm.WriteAsync(() =>
{ {
foreach (var b in imported.Beatmaps) foreach (var b in imported.Beatmaps)
b.OnlineID = -1; b.OnlineID = -1;

View File

@ -61,13 +61,13 @@ namespace osu.Game.Tests.Gameplay
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1)); Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
// No header shouldn't cause any change // No header shouldn't cause any change
scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame()); scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame());
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000)); Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000));
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1)); Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
// Reset with a miss instead. // Reset with a miss instead.
scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame
{ {
Header = new FrameHeader(0, 0, 0, new Dictionary<HitResult, int> { { HitResult.Miss, 1 } }, DateTimeOffset.Now) Header = new FrameHeader(0, 0, 0, new Dictionary<HitResult, int> { { HitResult.Miss, 1 } }, DateTimeOffset.Now)
}); });
@ -76,7 +76,7 @@ namespace osu.Game.Tests.Gameplay
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1)); Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
// Reset with no judged hit. // Reset with no judged hit.
scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame
{ {
Header = new FrameHeader(0, 0, 0, new Dictionary<HitResult, int>(), DateTimeOffset.Now) Header = new FrameHeader(0, 0, 0, new Dictionary<HitResult, int>(), DateTimeOffset.Now)
}); });

View File

@ -0,0 +1,48 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Game.Overlays;
using osu.Game.Screens.Edit.Timing;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneTapButton : OsuManualInputManagerTestScene
{
private TapButton tapButton;
[Cached]
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[Test]
public void TestBasic()
{
AddStep("create button", () =>
{
Child = tapButton = new TapButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(4),
};
});
bool pressed = false;
AddRepeatStep("Press button", () =>
{
InputManager.MoveMouseTo(tapButton);
if (!pressed)
InputManager.PressButton(MouseButton.Left);
else
InputManager.ReleaseButton(MouseButton.Left);
pressed = !pressed;
}, 100);
}
}
}

View File

@ -11,7 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Timing;
@ -77,34 +77,6 @@ namespace osu.Game.Tests.Visual.Editing
timingInfo.Text = $"offset: {selectedGroup.Value.Time:N2} bpm: {selectedGroup.Value.ControlPoints.OfType<TimingControlPoint>().First().BPM:N2}"; timingInfo.Text = $"offset: {selectedGroup.Value.Time:N2} bpm: {selectedGroup.Value.ControlPoints.OfType<TimingControlPoint>().First().BPM:N2}";
} }
[Test]
public void TestNoop()
{
AddStep("do nothing", () => { });
}
[Test]
public void TestTapThenReset()
{
AddStep("click tap button", () =>
{
control.ChildrenOfType<RoundedButton>()
.Last()
.TriggerClick();
});
AddUntilStep("wait for track playing", () => Clock.IsRunning);
AddStep("click reset button", () =>
{
control.ChildrenOfType<RoundedButton>()
.First()
.TriggerClick();
});
AddUntilStep("wait for track stopped", () => !Clock.IsRunning);
}
[Test] [Test]
public void TestBasic() public void TestBasic()
{ {
@ -115,7 +87,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("click tap button", () => AddStep("click tap button", () =>
{ {
control.ChildrenOfType<RoundedButton>() control.ChildrenOfType<OsuButton>()
.Last() .Last()
.TriggerClick(); .TriggerClick();
}); });
@ -129,6 +101,28 @@ namespace osu.Game.Tests.Visual.Editing
}); });
} }
[Test]
public void TestTapThenReset()
{
AddStep("click tap button", () =>
{
control.ChildrenOfType<OsuButton>()
.Last()
.TriggerClick();
});
AddUntilStep("wait for track playing", () => Clock.IsRunning);
AddStep("click reset button", () =>
{
control.ChildrenOfType<OsuButton>()
.First()
.TriggerClick();
});
AddUntilStep("wait for track stopped", () => !Clock.IsRunning);
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
Beatmap.Disabled = false; Beatmap.Disabled = false;

View File

@ -0,0 +1,48 @@
// 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.Utils;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneTimelineZoom : TimelineTestScene
{
public override Drawable CreateTestComponent() => Empty();
[Test]
public void TestVisibleRangeUpdatesOnZoomChange()
{
double initialVisibleRange = 0;
AddStep("reset zoom", () => TimelineArea.Timeline.Zoom = 100);
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("descale zoom", () => TimelineArea.Timeline.Zoom = 50);
AddAssert("range doubled", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange * 2, 1));
AddStep("restore zoom", () => TimelineArea.Timeline.Zoom = 100);
AddAssert("range restored", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange, 1));
}
[Test]
public void TestVisibleRangeConstantOnSizeChange()
{
double initialVisibleRange = 0;
AddStep("reset timeline size", () => TimelineArea.Timeline.Width = 1);
AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange);
AddStep("scale timeline size", () => TimelineArea.Timeline.Width = 2);
AddAssert("same range", () => TimelineArea.Timeline.VisibleRange == initialVisibleRange);
AddStep("descale timeline size", () => TimelineArea.Timeline.Width = 0.5f);
AddAssert("same range", () => TimelineArea.Timeline.VisibleRange == initialVisibleRange);
AddStep("restore timeline size", () => TimelineArea.Timeline.Width = 1);
AddAssert("same range", () => TimelineArea.Timeline.VisibleRange == initialVisibleRange);
}
}
}

View File

@ -44,7 +44,12 @@ namespace osu.Game.Tests.Visual.Editing
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(30) Colour = OsuColour.Gray(30)
}, },
scrollContainer = new ZoomableScrollContainer { RelativeSizeAxes = Axes.Both } scrollContainer = new ZoomableScrollContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
}
} }
}, },
new MenuCursor() new MenuCursor()
@ -62,7 +67,15 @@ namespace osu.Game.Tests.Visual.Editing
[Test] [Test]
public void TestWidthInitialization() public void TestWidthInitialization()
{ {
AddAssert("Inner container width was initialized", () => innerBox.DrawWidth > 0); AddAssert("Inner container width was initialized", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
}
[Test]
public void TestWidthUpdatesOnDrawSizeChanges()
{
AddStep("Shrink scroll container", () => scrollContainer.Width = 0.5f);
AddAssert("Scroll container width shrunk", () => scrollContainer.DrawWidth == scrollContainer.Parent.DrawWidth / 2);
AddAssert("Inner container width matches scroll container", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
} }
[Test] [Test]

View File

@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
protected readonly BeatmapDownloadTracker DownloadTracker; protected readonly BeatmapDownloadTracker DownloadTracker;
protected BeatmapCard(APIBeatmapSet beatmapSet, bool allowExpansion = true) protected BeatmapCard(APIBeatmapSet beatmapSet, bool allowExpansion = true)
: base(HoverSampleSet.Submit) : base(HoverSampleSet.Button)
{ {
Expanded = new BindableBool { Disabled = !allowExpansion }; Expanded = new BindableBool { Disabled = !allowExpansion };

View File

@ -6,7 +6,6 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Beatmaps.Drawables.Cards.Statistics; using osu.Game.Beatmaps.Drawables.Cards.Statistics;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -245,10 +244,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
}); });
if (BeatmapSet.HasVideo) if (BeatmapSet.HasVideo)
leftIconArea.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) }); leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(20) });
if (BeatmapSet.HasStoryboard) if (BeatmapSet.HasStoryboard)
leftIconArea.Add(new IconPill(FontAwesome.Solid.Image) { IconSize = new Vector2(20) }); leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(20) });
if (BeatmapSet.FeaturedInSpotlight) if (BeatmapSet.FeaturedInSpotlight)
{ {

View File

@ -7,7 +7,6 @@ using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Beatmaps.Drawables.Cards.Statistics; using osu.Game.Beatmaps.Drawables.Cards.Statistics;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -226,10 +225,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
}); });
if (BeatmapSet.HasVideo) if (BeatmapSet.HasVideo)
leftIconArea.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) }); leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(20) });
if (BeatmapSet.HasStoryboard) if (BeatmapSet.HasStoryboard)
leftIconArea.Add(new IconPill(FontAwesome.Solid.Image) { IconSize = new Vector2(20) }); leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(20) });
if (BeatmapSet.FeaturedInSpotlight) if (BeatmapSet.FeaturedInSpotlight)
{ {

View File

@ -3,14 +3,16 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Beatmaps.Drawables.Cards namespace osu.Game.Beatmaps.Drawables.Cards
{ {
public class IconPill : CircularContainer public abstract class IconPill : CircularContainer, IHasTooltip
{ {
public Vector2 IconSize public Vector2 IconSize
{ {
@ -20,7 +22,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
private readonly Container iconContainer; private readonly Container iconContainer;
public IconPill(IconUsage icon) protected IconPill(IconUsage icon)
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
Masking = true; Masking = true;
@ -47,5 +49,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
}, },
}; };
} }
public abstract LocalisableString TooltipText { get; }
} }
} }

View File

@ -0,0 +1,19 @@
// 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.Sprites;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Beatmaps.Drawables.Cards
{
public class StoryboardIconPill : IconPill
{
public StoryboardIconPill()
: base(FontAwesome.Solid.Image)
{
}
public override LocalisableString TooltipText => BeatmapsetsStrings.ShowInfoStoryboard;
}
}

View File

@ -0,0 +1,19 @@
// 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.Sprites;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Beatmaps.Drawables.Cards
{
public class VideoIconPill : IconPill
{
public VideoIconPill()
: base(FontAwesome.Solid.Film)
{
}
public override LocalisableString TooltipText => BeatmapsetsStrings.ShowInfoVideo;
}
}

View File

@ -392,7 +392,7 @@ namespace osu.Game.Database
{ {
total_writes_async.Value++; total_writes_async.Value++;
using (var realm = getRealmInstance()) using (var realm = getRealmInstance())
await realm.WriteAsync(action); await realm.WriteAsync(() => action(realm));
} }
/// <summary> /// <summary>

View File

@ -21,7 +21,7 @@ namespace osu.Game.Graphics.UserInterface
{ {
Size = TwoLayerButton.SIZE_EXTENDED; Size = TwoLayerButton.SIZE_EXTENDED;
Child = button = new TwoLayerButton(HoverSampleSet.Submit) Child = button = new TwoLayerButton
{ {
Anchor = Anchor.TopLeft, Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft, Origin = Anchor.TopLeft,

View File

@ -56,8 +56,8 @@ namespace osu.Game.Graphics.UserInterface
private readonly SpriteText spriteText; private readonly SpriteText spriteText;
private Vector2 hoverSpacing => new Vector2(3f, 0f); private Vector2 hoverSpacing => new Vector2(3f, 0f);
public DialogButton() public DialogButton(HoverSampleSet sampleSet = HoverSampleSet.Button)
: base(HoverSampleSet.Submit) : base(sampleSet)
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;

View File

@ -36,7 +36,7 @@ namespace osu.Game.Graphics.UserInterface
Icon = FontAwesome.Solid.ExternalLinkAlt, Icon = FontAwesome.Solid.ExternalLinkAlt,
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },
new HoverClickSounds(HoverSampleSet.Submit) new HoverClickSounds()
}; };
} }

View File

@ -7,6 +7,7 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
@ -37,7 +38,10 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
if (buttons.Contains(e.Button) && Contains(e.ScreenSpaceMousePosition)) if (buttons.Contains(e.Button) && Contains(e.ScreenSpaceMousePosition))
sampleClick?.Play(); {
sampleClick.Frequency.Value = 0.99 + RNG.NextDouble(0.02);
sampleClick.Play();
}
return base.OnClick(e); return base.OnClick(e);
} }

View File

@ -10,9 +10,6 @@ namespace osu.Game.Graphics.UserInterface
[Description("default")] [Description("default")]
Default, Default,
[Description("submit")]
Submit,
[Description("button")] [Description("button")]
Button, Button,

View File

@ -13,6 +13,7 @@ namespace osu.Game.Graphics.UserInterface
{ {
public class ShearedToggleButton : ShearedButton public class ShearedToggleButton : ShearedButton
{ {
private Sample? sampleClick;
private Sample? sampleOff; private Sample? sampleOff;
private Sample? sampleOn; private Sample? sampleOn;
@ -39,8 +40,9 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio) private void load(AudioManager audio)
{ {
sampleOn = audio.Samples.Get(@"UI/check-on"); sampleClick = audio.Samples.Get(@"UI/default-select");
sampleOff = audio.Samples.Get(@"UI/check-off"); sampleOn = audio.Samples.Get(@"UI/dropdown-open");
sampleOff = audio.Samples.Get(@"UI/dropdown-close");
} }
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet); protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
@ -67,6 +69,8 @@ namespace osu.Game.Graphics.UserInterface
private void playSample() private void playSample()
{ {
sampleClick?.Play();
if (Active.Value) if (Active.Value)
sampleOn?.Play(); sampleOn?.Play();
else else

View File

@ -80,6 +80,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight), new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight),
new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode), new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode),
new KeyBinding(new[] { InputKey.F5 }, GlobalAction.EditorTestGameplay), new KeyBinding(new[] { InputKey.F5 }, GlobalAction.EditorTestGameplay),
new KeyBinding(new[] { InputKey.T }, GlobalAction.EditorTapForBPM),
new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.EditorFlipHorizontally), new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.EditorFlipHorizontally),
new KeyBinding(new[] { InputKey.Control, InputKey.J }, GlobalAction.EditorFlipVertically), new KeyBinding(new[] { InputKey.Control, InputKey.J }, GlobalAction.EditorFlipVertically),
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.EditorDecreaseDistanceSpacing), new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.EditorDecreaseDistanceSpacing),
@ -322,5 +323,8 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DeselectAllMods))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DeselectAllMods))]
DeselectAllMods, DeselectAllMods,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTapForBPM))]
EditorTapForBPM,
} }
} }

View File

@ -174,6 +174,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString EditorTimingMode => new TranslatableString(getKey(@"editor_timing_mode"), @"Timing mode"); public static LocalisableString EditorTimingMode => new TranslatableString(getKey(@"editor_timing_mode"), @"Timing mode");
/// <summary>
/// "Tap for BPM"
/// </summary>
public static LocalisableString EditorTapForBPM => new TranslatableString(getKey(@"editor_tap_for_bpm"), @"Tap for BPM");
/// <summary> /// <summary>
/// "Cycle grid display mode" /// "Cycle grid display mode"
/// </summary> /// </summary>

View File

@ -24,6 +24,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString UnableToRunExclusiveFullscreen => new TranslatableString(getKey(@"unable_to_run_exclusive_fullscreen"), @"Unable to run exclusive fullscreen. You'll still experience some input latency."); public static LocalisableString UnableToRunExclusiveFullscreen => new TranslatableString(getKey(@"unable_to_run_exclusive_fullscreen"), @"Unable to run exclusive fullscreen. You'll still experience some input latency.");
/// <summary>
/// "Using fullscreen on macOS makes interacting with the menu bar and spaces no longer work, and may lead to freezes if a system dialog is presented. Using borderless is recommended."
/// </summary>
public static LocalisableString FullscreenMacOSNote => new TranslatableString(getKey(@"fullscreen_macos_note"), @"Using fullscreen on macOS makes interacting with the menu bar and spaces no longer work, and may lead to freezes if a system dialog is presented. Using borderless is recommended.");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -121,8 +121,16 @@ namespace osu.Game.Online.API
if (isFailing) return; if (isFailing) return;
Logger.Log($@"Performing request {this}", LoggingTarget.Network); try
WebRequest.Perform(); {
Logger.Log($@"Performing request {this}", LoggingTarget.Network);
WebRequest.Perform();
}
catch (OperationCanceledException)
{
// ignore this. internally Perform is running async and the fail state may have changed since
// the last check of `isFailing` above.
}
if (isFailing) return; if (isFailing) return;

View File

@ -38,7 +38,6 @@ namespace osu.Game.Online.Chat
} }
public DrawableLinkCompiler(IEnumerable<Drawable> parts) public DrawableLinkCompiler(IEnumerable<Drawable> parts)
: base(HoverSampleSet.Submit)
{ {
Parts = parts.ToList(); Parts = parts.ToList();
} }

View File

@ -197,6 +197,7 @@ namespace osu.Game.Online.Multiplayer
APIRoom.Playlist.Clear(); APIRoom.Playlist.Clear();
APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem)); APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem));
APIRoom.CurrentPlaylistItem.Value = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId);
Debug.Assert(LocalUser != null); Debug.Assert(LocalUser != null);
addUserToAPIRoom(LocalUser); addUserToAPIRoom(LocalUser);
@ -737,6 +738,7 @@ namespace osu.Game.Online.Multiplayer
APIRoom.Type.Value = Room.Settings.MatchType; APIRoom.Type.Value = Room.Settings.MatchType;
APIRoom.QueueMode.Value = Room.Settings.QueueMode; APIRoom.QueueMode.Value = Room.Settings.QueueMode;
APIRoom.AutoStartDuration.Value = Room.Settings.AutoStartDuration; APIRoom.AutoStartDuration.Value = Room.Settings.AutoStartDuration;
APIRoom.CurrentPlaylistItem.Value = APIRoom.Playlist.Single(item => item.ID == settings.PlaylistItemId);
RoomUpdated?.Invoke(); RoomUpdated?.Invoke();
} }

View File

@ -61,7 +61,13 @@ namespace osu.Game.Online.Rooms
/// Used for serialising to the API. /// Used for serialising to the API.
/// </summary> /// </summary>
[JsonProperty("beatmap_id")] [JsonProperty("beatmap_id")]
private int onlineBeatmapId => Beatmap.OnlineID; private int onlineBeatmapId
{
get => Beatmap.OnlineID;
// This setter is only required for client-side serialise-then-deserialise operations.
// Serialisation is supposed to emit only a `beatmap_id`, but a (non-null) `beatmap` is required on deserialise.
set => Beatmap = new APIBeatmap { OnlineID = value };
}
/// <summary> /// <summary>
/// A beatmap representing this playlist item. /// A beatmap representing this playlist item.

View File

@ -162,6 +162,13 @@ namespace osu.Game.Online.Rooms
Password.BindValueChanged(p => HasPassword.Value = !string.IsNullOrEmpty(p.NewValue)); Password.BindValueChanged(p => HasPassword.Value = !string.IsNullOrEmpty(p.NewValue));
} }
/// <summary>
/// Copies values from another <see cref="Room"/> into this one.
/// </summary>
/// <remarks>
/// **Beware**: This will store references between <see cref="Room"/>s.
/// </remarks>
/// <param name="other">The <see cref="Room"/> to copy values from.</param>
public void CopyFrom(Room other) public void CopyFrom(Room other)
{ {
RoomID.Value = other.RoomID.Value; RoomID.Value = other.RoomID.Value;

View File

@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Humanizer; using Humanizer;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -658,11 +659,14 @@ namespace osu.Game
} }
protected override IDictionary<FrameworkSetting, object> GetFrameworkConfigDefaults() protected override IDictionary<FrameworkSetting, object> GetFrameworkConfigDefaults()
=> new Dictionary<FrameworkSetting, object> {
return new Dictionary<FrameworkSetting, object>
{ {
// General expectation that osu! starts in fullscreen by default (also gives the most predictable performance) // General expectation that osu! starts in fullscreen by default (also gives the most predictable performance).
{ FrameworkSetting.WindowMode, WindowMode.Fullscreen } // However, macOS is bound to have issues when using exclusive fullscreen as it takes full control away from OS, therefore borderless is default there.
{ FrameworkSetting.WindowMode, RuntimeInfo.OS == RuntimeInfo.Platform.macOS ? WindowMode.Borderless : WindowMode.Fullscreen }
}; };
}
protected override void LoadComplete() protected override void LoadComplete()
{ {

View File

@ -80,6 +80,9 @@ namespace osu.Game
public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild; public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild;
internal EndpointConfiguration CreateEndpoints() =>
UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version(); public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version();
/// <summary> /// <summary>
@ -268,7 +271,7 @@ namespace osu.Game
dependencies.Cache(SkinManager = new SkinManager(Storage, realm, Host, Resources, Audio, Scheduler)); dependencies.Cache(SkinManager = new SkinManager(Storage, realm, Host, Resources, Audio, Scheduler));
dependencies.CacheAs<ISkinSource>(SkinManager); dependencies.CacheAs<ISkinSource>(SkinManager);
EndpointConfiguration endpoints = UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration(); EndpointConfiguration endpoints = CreateEndpoints();
MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl; MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl;

View File

@ -5,6 +5,7 @@ using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Utils; using osu.Framework.Utils;
@ -69,7 +70,7 @@ namespace osu.Game.Overlays.AccountCreation
}, },
usernameTextBox = new OsuTextBox usernameTextBox = new OsuTextBox
{ {
PlaceholderText = UsersStrings.LoginUsername, PlaceholderText = UsersStrings.LoginUsername.ToLower(),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
TabbableContentContainer = this TabbableContentContainer = this
}, },
@ -91,7 +92,7 @@ namespace osu.Game.Overlays.AccountCreation
}, },
passwordTextBox = new OsuPasswordTextBox passwordTextBox = new OsuPasswordTextBox
{ {
PlaceholderText = "password", PlaceholderText = UsersStrings.LoginPassword.ToLower(),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
TabbableContentContainer = this, TabbableContentContainer = this,
}, },

View File

@ -40,7 +40,7 @@ namespace osu.Game.Overlays.BeatmapListing
Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular), Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular),
Text = LabelFor(Value) Text = LabelFor(Value)
}, },
new HoverClickSounds() new HoverClickSounds(HoverSampleSet.TabSelect)
}); });
Enabled.Value = true; Enabled.Value = true;

View File

@ -6,6 +6,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -16,6 +18,7 @@ using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Chat; using osu.Game.Online.Chat;
using osuTK; using osuTK;
@ -32,6 +35,8 @@ namespace osu.Game.Overlays.Chat.Listing
public IEnumerable<LocalisableString> FilterTerms => new LocalisableString[] { Channel.Name, Channel.Topic ?? string.Empty }; public IEnumerable<LocalisableString> FilterTerms => new LocalisableString[] { Channel.Name, Channel.Topic ?? string.Empty };
public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); } public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); }
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds();
private Box hoverBox = null!; private Box hoverBox = null!;
private SpriteIcon checkbox = null!; private SpriteIcon checkbox = null!;
private OsuSpriteText channelText = null!; private OsuSpriteText channelText = null!;
@ -46,14 +51,20 @@ namespace osu.Game.Overlays.Chat.Listing
private const float vertical_margin = 1.5f; private const float vertical_margin = 1.5f;
private Sample? sampleJoin;
private Sample? sampleLeave;
public ChannelListingItem(Channel channel) public ChannelListingItem(Channel channel)
{ {
Channel = channel; Channel = channel;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(AudioManager audio)
{ {
sampleJoin = audio.Samples.Get(@"UI/check-on");
sampleLeave = audio.Samples.Get(@"UI/check-off");
Masking = true; Masking = true;
CornerRadius = 5; CornerRadius = 5;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
@ -156,7 +167,19 @@ namespace osu.Game.Overlays.Chat.Listing
} }
}, true); }, true);
Action = () => (channelJoined.Value ? OnRequestLeave : OnRequestJoin)?.Invoke(Channel); Action = () =>
{
if (channelJoined.Value)
{
OnRequestLeave?.Invoke(Channel);
sampleLeave?.Play();
}
else
{
OnRequestJoin?.Invoke(Channel);
sampleJoin?.Play();
}
};
} }
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)

View File

@ -14,7 +14,6 @@ using osu.Framework.Platform;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Overlays.News namespace osu.Game.Overlays.News
@ -29,7 +28,6 @@ namespace osu.Game.Overlays.News
private TextFlowContainer main; private TextFlowContainer main;
public NewsCard(APINewsPost post) public NewsCard(APINewsPost post)
: base(HoverSampleSet.Submit)
{ {
this.post = post; this.post = post;

View File

@ -18,7 +18,6 @@ using System.Diagnostics;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.News.Sidebar namespace osu.Game.Overlays.News.Sidebar
{ {
@ -129,7 +128,6 @@ namespace osu.Game.Overlays.News.Sidebar
private readonly APINewsPost post; private readonly APINewsPost post;
public PostButton(APINewsPost post) public PostButton(APINewsPost post)
: base(HoverSampleSet.Submit)
{ {
this.post = post; this.post = post;

View File

@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
private Sample sampleOpen; private Sample sampleOpen;
private Sample sampleClose; private Sample sampleClose;
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(); protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds();
public ExpandDetailsButton() public ExpandDetailsButton()
{ {

View File

@ -6,7 +6,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Profile.Sections namespace osu.Game.Overlays.Profile.Sections
{ {
@ -18,7 +17,6 @@ namespace osu.Game.Overlays.Profile.Sections
private readonly IBeatmapInfo beatmapInfo; private readonly IBeatmapInfo beatmapInfo;
protected BeatmapMetadataContainer(IBeatmapInfo beatmapInfo) protected BeatmapMetadataContainer(IBeatmapInfo beatmapInfo)
: base(HoverSampleSet.Submit)
{ {
this.beatmapInfo = beatmapInfo; this.beatmapInfo = beatmapInfo;

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Configuration; using osu.Framework.Configuration;
@ -224,6 +225,16 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private void updateScreenModeWarning() private void updateScreenModeWarning()
{ {
if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS)
{
if (windowModeDropdown.Current.Value == WindowMode.Fullscreen)
windowModeDropdown.SetNoticeText(LayoutSettingsStrings.FullscreenMacOSNote, true);
else
windowModeDropdown.ClearNoticeText();
return;
}
if (windowModeDropdown.Current.Value != WindowMode.Fullscreen) if (windowModeDropdown.Current.Value != WindowMode.Fullscreen)
{ {
windowModeDropdown.SetNoticeText(GraphicsSettingsStrings.NotFullscreenNote, true); windowModeDropdown.SetNoticeText(GraphicsSettingsStrings.NotFullscreenNote, true);

View File

@ -20,7 +20,6 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -83,7 +82,6 @@ namespace osu.Game.Overlays.Toolbar
private RealmAccess realm { get; set; } private RealmAccess realm { get; set; }
protected ToolbarButton() protected ToolbarButton()
: base(HoverSampleSet.Toolbar)
{ {
Width = Toolbar.HEIGHT; Width = Toolbar.HEIGHT;
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;

View File

@ -11,7 +11,6 @@ using osu.Framework.Input.Events;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -29,7 +28,6 @@ namespace osu.Game.Overlays.Toolbar
private AnalogClockDisplay analog; private AnalogClockDisplay analog;
public ToolbarClock() public ToolbarClock()
: base(HoverSampleSet.Toolbar)
{ {
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X; AutoSizeAxes = Axes.X;

View File

@ -117,9 +117,8 @@ namespace osu.Game.Rulesets.Scoring
/// <remarks> /// <remarks>
/// If the provided replay frame does not have any header information, this will be a noop. /// If the provided replay frame does not have any header information, this will be a noop.
/// </remarks> /// </remarks>
/// <param name="ruleset">The ruleset to be used for retrieving statistics.</param>
/// <param name="frame">The replay frame to read header statistics from.</param> /// <param name="frame">The replay frame to read header statistics from.</param>
public virtual void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame) public virtual void ResetFromReplayFrame(ReplayFrame frame)
{ {
if (frame.Header == null) if (frame.Header == null)
return; return;

View File

@ -6,11 +6,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -88,17 +90,34 @@ namespace osu.Game.Rulesets.Scoring
private readonly double accuracyPortion; private readonly double accuracyPortion;
private readonly double comboPortion; private readonly double comboPortion;
private int maxAchievableCombo; /// <summary>
/// Scoring values for a perfect play.
/// </summary>
public ScoringValues MaximumScoringValues
{
get
{
if (!beatmapApplied)
throw new InvalidOperationException($"Cannot access maximum scoring values before calling {nameof(ApplyBeatmap)}.");
return maximumScoringValues;
}
}
private ScoringValues maximumScoringValues;
/// <summary> /// <summary>
/// The maximum achievable base score. /// Scoring values for the current play assuming all perfect hits.
/// </summary> /// </summary>
private double maxBaseScore; /// <remarks>
/// This is only used to determine the accuracy with respect to the current point in time for an ongoing play session.
/// </remarks>
private ScoringValues currentMaximumScoringValues;
/// <summary> /// <summary>
/// The maximum number of basic (non-tick and non-bonus) hitobjects. /// Scoring values for the current play.
/// </summary> /// </summary>
private int maxBasicHitObjects; private ScoringValues currentScoringValues;
/// <summary> /// <summary>
/// The maximum <see cref="HitResult"/> of a basic (non-tick and non-bonus) hitobject. /// The maximum <see cref="HitResult"/> of a basic (non-tick and non-bonus) hitobject.
@ -106,9 +125,6 @@ namespace osu.Game.Rulesets.Scoring
/// </summary> /// </summary>
private HitResult? maxBasicResult; private HitResult? maxBasicResult;
private double rollingMaxBaseScore;
private double baseScore;
private int basicHitObjects;
private bool beatmapApplied; private bool beatmapApplied;
private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>(); private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>();
@ -163,6 +179,10 @@ namespace osu.Game.Rulesets.Scoring
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1; scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1;
// Always update the maximum scoring values.
applyResult(result.Judgement.MaxResult, ref currentMaximumScoringValues);
currentMaximumScoringValues.MaxCombo += result.Judgement.MaxResult.IncreasesCombo() ? 1 : 0;
if (!result.Type.IsScorable()) if (!result.Type.IsScorable())
return; return;
@ -171,16 +191,8 @@ namespace osu.Game.Rulesets.Scoring
else if (result.Type.BreaksCombo()) else if (result.Type.BreaksCombo())
Combo.Value = 0; Combo.Value = 0;
double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0; applyResult(result.Type, ref currentScoringValues);
currentScoringValues.MaxCombo = HighestCombo.Value;
if (!result.Type.IsBonus())
{
baseScore += scoreIncrease;
rollingMaxBaseScore += result.Judgement.MaxNumericResult;
}
if (result.Type.IsBasic())
basicHitObjects++;
hitEvents.Add(CreateHitEvent(result)); hitEvents.Add(CreateHitEvent(result));
lastHitObject = result.HitObject; lastHitObject = result.HitObject;
@ -188,6 +200,20 @@ namespace osu.Game.Rulesets.Scoring
updateScore(); updateScore();
} }
private static void applyResult(HitResult result, ref ScoringValues scoringValues)
{
if (!result.IsScorable())
return;
if (result.IsBonus())
scoringValues.BonusScore += result.IsHit() ? Judgement.ToNumericResult(result) : 0;
else
scoringValues.BaseScore += result.IsHit() ? Judgement.ToNumericResult(result) : 0;
if (result.IsBasic())
scoringValues.CountBasicHitObjects++;
}
/// <summary> /// <summary>
/// Creates the <see cref="HitEvent"/> that describes a <see cref="JudgementResult"/>. /// Creates the <see cref="HitEvent"/> that describes a <see cref="JudgementResult"/>.
/// </summary> /// </summary>
@ -206,19 +232,15 @@ namespace osu.Game.Rulesets.Scoring
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1; scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1;
// Always update the maximum scoring values.
revertResult(result.Judgement.MaxResult, ref currentMaximumScoringValues);
currentMaximumScoringValues.MaxCombo -= result.Judgement.MaxResult.IncreasesCombo() ? 1 : 0;
if (!result.Type.IsScorable()) if (!result.Type.IsScorable())
return; return;
double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0; revertResult(result.Type, ref currentScoringValues);
currentScoringValues.MaxCombo = HighestCombo.Value;
if (!result.Type.IsBonus())
{
baseScore -= scoreIncrease;
rollingMaxBaseScore -= result.Judgement.MaxNumericResult;
}
if (result.Type.IsBasic())
basicHitObjects--;
Debug.Assert(hitEvents.Count > 0); Debug.Assert(hitEvents.Count > 0);
lastHitObject = hitEvents[^1].LastHitObject; lastHitObject = hitEvents[^1].LastHitObject;
@ -227,14 +249,24 @@ namespace osu.Game.Rulesets.Scoring
updateScore(); updateScore();
} }
private static void revertResult(HitResult result, ref ScoringValues scoringValues)
{
if (!result.IsScorable())
return;
if (result.IsBonus())
scoringValues.BonusScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0;
else
scoringValues.BaseScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0;
if (result.IsBasic())
scoringValues.CountBasicHitObjects--;
}
private void updateScore() private void updateScore()
{ {
double rollingAccuracyRatio = rollingMaxBaseScore > 0 ? baseScore / rollingMaxBaseScore : 1; Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1;
double accuracyRatio = maxBaseScore > 0 ? baseScore / maxBaseScore : 1; TotalScore.Value = ComputeScore(Mode.Value, currentScoringValues, maximumScoringValues);
double comboRatio = maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1;
Accuracy.Value = rollingAccuracyRatio;
TotalScore.Value = ComputeScore(Mode.Value, accuracyRatio, comboRatio, getBonusScore(scoreResultCounts), maxBasicHitObjects);
} }
/// <summary> /// <summary>
@ -246,22 +278,15 @@ namespace osu.Game.Rulesets.Scoring
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param> /// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param> /// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns> /// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
[Pure]
public double ComputeFinalScore(ScoringMode mode, ScoreInfo scoreInfo) public double ComputeFinalScore(ScoringMode mode, ScoreInfo scoreInfo)
{ {
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
extractFromStatistics(ruleset, ExtractScoringValues(scoreInfo, out var current, out var maximum);
scoreInfo.Statistics,
out double extractedBaseScore,
out double extractedMaxBaseScore,
out int extractedMaxCombo,
out int extractedBasicHitObjects);
double accuracyRatio = extractedMaxBaseScore > 0 ? extractedBaseScore / extractedMaxBaseScore : 1; return ComputeScore(mode, current, maximum);
double comboRatio = extractedMaxCombo > 0 ? (double)scoreInfo.MaxCombo / extractedMaxCombo : 1;
return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), extractedBasicHitObjects);
} }
/// <summary> /// <summary>
@ -273,6 +298,7 @@ namespace osu.Game.Rulesets.Scoring
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param> /// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param> /// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns> /// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
[Pure]
public double ComputePartialScore(ScoringMode mode, ScoreInfo scoreInfo) public double ComputePartialScore(ScoringMode mode, ScoreInfo scoreInfo)
{ {
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
@ -281,17 +307,9 @@ namespace osu.Game.Rulesets.Scoring
if (!beatmapApplied) if (!beatmapApplied)
throw new InvalidOperationException($"Cannot compute partial score without calling {nameof(ApplyBeatmap)}."); throw new InvalidOperationException($"Cannot compute partial score without calling {nameof(ApplyBeatmap)}.");
extractFromStatistics(ruleset, ExtractScoringValues(scoreInfo, out var current, out _);
scoreInfo.Statistics,
out double extractedBaseScore,
out _,
out _,
out _);
double accuracyRatio = maxBaseScore > 0 ? extractedBaseScore / maxBaseScore : 1; return ComputeScore(mode, current, MaximumScoringValues);
double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1;
return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), maxBasicHitObjects);
} }
/// <summary> /// <summary>
@ -305,6 +323,7 @@ namespace osu.Game.Rulesets.Scoring
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param> /// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
/// <param name="maxAchievableCombo">The maximum achievable combo for the provided beatmap.</param> /// <param name="maxAchievableCombo">The maximum achievable combo for the provided beatmap.</param>
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns> /// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
[Pure]
public double ComputeFinalLegacyScore(ScoringMode mode, ScoreInfo scoreInfo, int maxAchievableCombo) public double ComputeFinalLegacyScore(ScoringMode mode, ScoreInfo scoreInfo, int maxAchievableCombo)
{ {
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
@ -313,26 +332,30 @@ namespace osu.Game.Rulesets.Scoring
double accuracyRatio = scoreInfo.Accuracy; double accuracyRatio = scoreInfo.Accuracy;
double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1; double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1;
ExtractScoringValues(scoreInfo, out var current, out var maximum);
// For legacy osu!mania scores, a full-GREAT score has 100% accuracy. If combined with a full-combo, the score becomes indistinguishable from a full-PERFECT score. // For legacy osu!mania scores, a full-GREAT score has 100% accuracy. If combined with a full-combo, the score becomes indistinguishable from a full-PERFECT score.
// To get around this, the accuracy ratio is always recalculated based on the hit statistics rather than trusting the score. // To get around this, the accuracy ratio is always recalculated based on the hit statistics rather than trusting the score.
// Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together. // Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together.
if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3) if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3 && maximum.BaseScore > 0)
{ accuracyRatio = current.BaseScore / maximum.BaseScore;
extractFromStatistics(
ruleset,
scoreInfo.Statistics,
out double computedBaseScore,
out double computedMaxBaseScore,
out _,
out _);
if (computedMaxBaseScore > 0) return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.CountBasicHitObjects);
accuracyRatio = computedBaseScore / computedMaxBaseScore; }
}
int computedBasicHitObjects = scoreInfo.Statistics.Where(kvp => kvp.Key.IsBasic()).Select(kvp => kvp.Value).Sum(); /// <summary>
/// Computes the total score from scoring values.
return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), computedBasicHitObjects); /// </summary>
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
/// <param name="current">The current scoring values.</param>
/// <param name="maximum">The maximum scoring values.</param>
/// <returns>The total score computed from the given scoring values.</returns>
[Pure]
public double ComputeScore(ScoringMode mode, ScoringValues current, ScoringValues maximum)
{
double accuracyRatio = maximum.BaseScore > 0 ? current.BaseScore / maximum.BaseScore : 1;
double comboRatio = maximum.MaxCombo > 0 ? (double)current.MaxCombo / maximum.MaxCombo : 1;
return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.CountBasicHitObjects);
} }
/// <summary> /// <summary>
@ -344,6 +367,7 @@ namespace osu.Game.Rulesets.Scoring
/// <param name="bonusScore">The total bonus score.</param> /// <param name="bonusScore">The total bonus score.</param>
/// <param name="totalBasicHitObjects">The total number of basic (non-tick and non-bonus) hitobjects in the beatmap.</param> /// <param name="totalBasicHitObjects">The total number of basic (non-tick and non-bonus) hitobjects in the beatmap.</param>
/// <returns>The total score computed from the given scoring component ratios.</returns> /// <returns>The total score computed from the given scoring component ratios.</returns>
[Pure]
public double ComputeScore(ScoringMode mode, double accuracyRatio, double comboRatio, double bonusScore, int totalBasicHitObjects) public double ComputeScore(ScoringMode mode, double accuracyRatio, double comboRatio, double bonusScore, int totalBasicHitObjects)
{ {
switch (mode) switch (mode)
@ -362,15 +386,6 @@ namespace osu.Game.Rulesets.Scoring
} }
} }
/// <summary>
/// Calculates the total bonus score from score statistics.
/// </summary>
/// <param name="statistics">The score statistics.</param>
/// <returns>The total bonus score.</returns>
private double getBonusScore(IReadOnlyDictionary<HitResult, int> statistics)
=> statistics.GetValueOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE
+ statistics.GetValueOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE;
private ScoreRank rankFrom(double acc) private ScoreRank rankFrom(double acc)
{ {
if (acc == 1) if (acc == 1)
@ -402,15 +417,10 @@ namespace osu.Game.Rulesets.Scoring
lastHitObject = null; lastHitObject = null;
if (storeResults) if (storeResults)
{ maximumScoringValues = currentScoringValues;
maxAchievableCombo = HighestCombo.Value;
maxBaseScore = baseScore;
maxBasicHitObjects = basicHitObjects;
}
baseScore = 0; currentScoringValues = default;
rollingMaxBaseScore = 0; currentMaximumScoringValues = default;
basicHitObjects = 0;
TotalScore.Value = 0; TotalScore.Value = 0;
Accuracy.Value = 1; Accuracy.Value = 1;
@ -437,14 +447,19 @@ namespace osu.Game.Rulesets.Scoring
score.TotalScore = (long)Math.Round(ComputeFinalScore(ScoringMode.Standardised, score)); score.TotalScore = (long)Math.Round(ComputeFinalScore(ScoringMode.Standardised, score));
} }
public override void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame) public override void ResetFromReplayFrame(ReplayFrame frame)
{ {
base.ResetFromReplayFrame(ruleset, frame); base.ResetFromReplayFrame(frame);
if (frame.Header == null) if (frame.Header == null)
return; return;
extractFromStatistics(ruleset, frame.Header.Statistics, out baseScore, out rollingMaxBaseScore, out _, out _); extractScoringValues(frame.Header.Statistics, out var current, out var maximum);
currentScoringValues.BaseScore = current.BaseScore;
currentScoringValues.MaxCombo = frame.Header.MaxCombo;
currentMaximumScoringValues.BaseScore = maximum.BaseScore;
currentMaximumScoringValues.MaxCombo = maximum.MaxCombo;
HighestCombo.Value = frame.Header.MaxCombo; HighestCombo.Value = frame.Header.MaxCombo;
scoreResultCounts.Clear(); scoreResultCounts.Clear();
@ -455,52 +470,126 @@ namespace osu.Game.Rulesets.Scoring
OnResetFromReplayFrame?.Invoke(); OnResetFromReplayFrame?.Invoke();
} }
private void extractFromStatistics(Ruleset ruleset, IReadOnlyDictionary<HitResult, int> statistics, out double baseScore, out double maxBaseScore, out int maxCombo, #region ScoringValue extraction
out int basicHitObjects)
/// <summary>
/// Applies a best-effort extraction of hit statistics into <see cref="ScoringValues"/>.
/// </summary>
/// <remarks>
/// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
/// <list type="bullet">
/// <item>The maximum <see cref="ScoringValues.BonusScore"/> will always be 0.</item>
/// <item>The current and maximum <see cref="ScoringValues.CountBasicHitObjects"/> will always be the same value.</item>
/// </list>
/// Consumers are expected to more accurately fill in the above values through external means.
/// <para>
/// <b>Ensure</b> to fill in the maximum <see cref="ScoringValues.CountBasicHitObjects"/> for use in
/// <see cref="ComputeScore(osu.Game.Rulesets.Scoring.ScoringMode,osu.Game.Scoring.ScoringValues,osu.Game.Scoring.ScoringValues)"/>.
/// </para>
/// </remarks>
/// <param name="scoreInfo">The score to extract scoring values from.</param>
/// <param name="current">The "current" scoring values, representing the hit statistics as they appear.</param>
/// <param name="maximum">The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.</param>
[Pure]
internal void ExtractScoringValues(ScoreInfo scoreInfo, out ScoringValues current, out ScoringValues maximum)
{ {
baseScore = 0; extractScoringValues(scoreInfo.Statistics, out current, out maximum);
maxBaseScore = 0; current.MaxCombo = scoreInfo.MaxCombo;
maxCombo = 0; }
basicHitObjects = 0;
/// <summary>
/// Applies a best-effort extraction of hit statistics into <see cref="ScoringValues"/>.
/// </summary>
/// <remarks>
/// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
/// <list type="bullet">
/// <item>The maximum <see cref="ScoringValues.BonusScore"/> will always be 0.</item>
/// <item>The current and maximum <see cref="ScoringValues.CountBasicHitObjects"/> will always be the same value.</item>
/// </list>
/// Consumers are expected to more accurately fill in the above values through external means.
/// <para>
/// <b>Ensure</b> to fill in the maximum <see cref="ScoringValues.CountBasicHitObjects"/> for use in
/// <see cref="ComputeScore(osu.Game.Rulesets.Scoring.ScoringMode,osu.Game.Scoring.ScoringValues,osu.Game.Scoring.ScoringValues)"/>.
/// </para>
/// </remarks>
/// <param name="header">The replay frame header to extract scoring values from.</param>
/// <param name="current">The "current" scoring values, representing the hit statistics as they appear.</param>
/// <param name="maximum">The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.</param>
[Pure]
internal void ExtractScoringValues(FrameHeader header, out ScoringValues current, out ScoringValues maximum)
{
extractScoringValues(header.Statistics, out current, out maximum);
current.MaxCombo = header.MaxCombo;
}
/// <summary>
/// Applies a best-effort extraction of hit statistics into <see cref="ScoringValues"/>.
/// </summary>
/// <remarks>
/// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
/// <list type="bullet">
/// <item>The current <see cref="ScoringValues.MaxCombo"/> will always be 0.</item>
/// <item>The maximum <see cref="ScoringValues.BonusScore"/> will always be 0.</item>
/// <item>The current and maximum <see cref="ScoringValues.CountBasicHitObjects"/> will always be the same value.</item>
/// </list>
/// Consumers are expected to more accurately fill in the above values (especially the current <see cref="ScoringValues.MaxCombo"/>) via external means (e.g. <see cref="ScoreInfo"/>).
/// </remarks>
/// <param name="statistics">The hit statistics to extract scoring values from.</param>
/// <param name="current">The "current" scoring values, representing the hit statistics as they appear.</param>
/// <param name="maximum">The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.</param>
[Pure]
private void extractScoringValues(IReadOnlyDictionary<HitResult, int> statistics, out ScoringValues current, out ScoringValues maximum)
{
current = default;
maximum = default;
foreach ((HitResult result, int count) in statistics) foreach ((HitResult result, int count) in statistics)
{ {
// Bonus scores are counted separately directly from the statistics dictionary later on. if (!result.IsScorable())
if (!result.IsScorable() || result.IsBonus())
continue; continue;
// The maximum result of this judgement if it wasn't a miss. if (result.IsBonus())
// E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT). current.BonusScore += count * Judgement.ToNumericResult(result);
HitResult maxResult; else
switch (result)
{ {
case HitResult.LargeTickHit: // The maximum result of this judgement if it wasn't a miss.
case HitResult.LargeTickMiss: // E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT).
maxResult = HitResult.LargeTickHit; HitResult maxResult;
break;
case HitResult.SmallTickHit: switch (result)
case HitResult.SmallTickMiss: {
maxResult = HitResult.SmallTickHit; case HitResult.LargeTickHit:
break; case HitResult.LargeTickMiss:
maxResult = HitResult.LargeTickHit;
break;
default: case HitResult.SmallTickHit:
maxResult = maxBasicResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result; case HitResult.SmallTickMiss:
break; maxResult = HitResult.SmallTickHit;
break;
default:
maxResult = maxBasicResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result;
break;
}
current.BaseScore += count * Judgement.ToNumericResult(result);
maximum.BaseScore += count * Judgement.ToNumericResult(maxResult);
} }
baseScore += count * Judgement.ToNumericResult(result);
maxBaseScore += count * Judgement.ToNumericResult(maxResult);
if (result.AffectsCombo()) if (result.AffectsCombo())
maxCombo += count; maximum.MaxCombo += count;
if (result.IsBasic()) if (result.IsBasic())
basicHitObjects += count; {
current.CountBasicHitObjects += count;
maximum.CountBasicHitObjects += count;
}
} }
} }
#endregion
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);

View File

@ -27,8 +27,6 @@ namespace osu.Game.Rulesets.UI
{ {
public readonly KeyBindingContainer<T> KeyBindingContainer; public readonly KeyBindingContainer<T> KeyBindingContainer;
private readonly Ruleset ruleset;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private ScoreProcessor scoreProcessor { get; set; } private ScoreProcessor scoreProcessor { get; set; }
@ -57,8 +55,6 @@ namespace osu.Game.Rulesets.UI
protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
{ {
this.ruleset = ruleset.CreateInstance();
InternalChild = KeyBindingContainer = InternalChild = KeyBindingContainer =
CreateKeyBindingContainer(ruleset, variant, unique) CreateKeyBindingContainer(ruleset, variant, unique)
.WithChild(content = new Container { RelativeSizeAxes = Axes.Both }); .WithChild(content = new Container { RelativeSizeAxes = Axes.Both });
@ -85,7 +81,7 @@ namespace osu.Game.Rulesets.UI
break; break;
case ReplayStatisticsFrameEvent statisticsStateChangeEvent: case ReplayStatisticsFrameEvent statisticsStateChangeEvent:
scoreProcessor?.ResetFromReplayFrame(ruleset, statisticsStateChangeEvent.Frame); scoreProcessor?.ResetFromReplayFrame(statisticsStateChangeEvent.Frame);
break; break;
default: default:

View File

@ -0,0 +1,41 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using MessagePack;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Scoring
{
/// <summary>
/// Stores the required scoring data that fulfils the minimum requirements for a <see cref="ScoreProcessor"/> to calculate score.
/// </summary>
[MessagePackObject]
public struct ScoringValues
{
/// <summary>
/// The sum of all "basic" <see cref="HitObject"/> scoring values. See: <see cref="HitResultExtensions.IsBasic"/> and <see cref="Judgement.ToNumericResult"/>.
/// </summary>
[Key(0)]
public double BaseScore;
/// <summary>
/// The sum of all "bonus" <see cref="HitObject"/> scoring values. See: <see cref="HitResultExtensions.IsBonus"/> and <see cref="Judgement.ToNumericResult"/>.
/// </summary>
[Key(1)]
public double BonusScore;
/// <summary>
/// The highest achieved combo.
/// </summary>
[Key(2)]
public int MaxCombo;
/// <summary>
/// The count of "basic" <see cref="HitObject"/>s. See: <see cref="HitResultExtensions.IsBasic"/>.
/// </summary>
[Key(3)]
public int CountBasicHitObjects;
}
}

View File

@ -1,99 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osuTK; using osu.Game.Overlays;
using osuTK.Graphics; using osu.Game.Screens.Edit.Timing;
using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
public class TimelineButton : CompositeDrawable public class TimelineButton : IconButton
{ {
public Action Action; [BackgroundDependencyLoader]
public readonly BindableBool Enabled = new BindableBool(true); private void load(OverlayColourProvider colourProvider)
public IconUsage Icon
{ {
get => button.Icon; // These are using colourProvider but don't match the design.
set => button.Icon = value; // Just something to fit until someone implements the updated design.
IconColour = colourProvider.Background1;
IconHoverColour = colourProvider.Content2;
HoverColour = colourProvider.Background1;
FlashColour = colourProvider.Content2;
Add(new RepeatingButtonBehaviour(this));
} }
private readonly IconButton button; protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
public TimelineButton()
{
InternalChild = button = new TimelineIconButton { Action = () => Action?.Invoke() };
button.Enabled.BindTo(Enabled);
Width = button.Width;
}
protected override void Update()
{
base.Update();
button.Size = new Vector2(button.Width, DrawHeight);
}
private class TimelineIconButton : IconButton
{
public TimelineIconButton()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
IconColour = OsuColour.Gray(0.35f);
IconHoverColour = Color4.White;
HoverColour = OsuColour.Gray(0.25f);
FlashColour = OsuColour.Gray(0.5f);
}
private ScheduledDelegate repeatSchedule;
/// <summary>
/// The initial delay before mouse down repeat begins.
/// </summary>
private const int repeat_initial_delay = 250;
/// <summary>
/// The delay between mouse down repeats after the initial repeat.
/// </summary>
private const int repeat_tick_rate = 70;
protected override bool OnClick(ClickEvent e)
{
// don't actuate a click since we are manually handling repeats.
return true;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button == MouseButton.Left)
{
Action clickAction = () => base.OnClick(new ClickEvent(e.CurrentState, e.Button));
// run once for initial down
clickAction();
Scheduler.Add(repeatSchedule = new ScheduledDelegate(clickAction, Clock.CurrentTime + repeat_initial_delay, repeat_tick_rate));
}
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
repeatSchedule?.Cancel();
base.OnMouseUp(e);
}
}
} }
} }

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Transforms; using osu.Framework.Graphics.Transforms;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -40,10 +41,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private IFrameBasedClock editorClock { get; set; } private IFrameBasedClock editorClock { get; set; }
private readonly LayoutValue zoomedContentWidthCache = new LayoutValue(Invalidation.DrawSize);
public ZoomableScrollContainer() public ZoomableScrollContainer()
: base(Direction.Horizontal) : base(Direction.Horizontal)
{ {
base.Content.Add(zoomedContent = new Container { RelativeSizeAxes = Axes.Y }); base.Content.Add(zoomedContent = new Container { RelativeSizeAxes = Axes.Y });
AddLayout(zoomedContentWidthCache);
} }
private float minZoom = 1; private float minZoom = 1;
@ -103,12 +108,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
} }
} }
protected override void LoadComplete() protected override void Update()
{ {
base.LoadComplete(); base.Update();
// This width only gets updated on the application of a transform, so this needs to be initialized here. if (!zoomedContentWidthCache.IsValid)
updateZoomedContentWidth(); updateZoomedContentWidth();
} }
protected override bool OnScroll(ScrollEvent e) protected override bool OnScroll(ScrollEvent e)
@ -128,7 +133,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return base.OnScroll(e); return base.OnScroll(e);
} }
private void updateZoomedContentWidth() => zoomedContent.Width = DrawWidth * currentZoom; private void updateZoomedContentWidth()
{
zoomedContent.Width = DrawWidth * currentZoom;
zoomedContentWidthCache.Validate();
}
private float zoomTarget = 1; private float zoomTarget = 1;
@ -199,8 +208,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
float targetOffset = expectedWidth * (focusPoint / contentSize) - focusOffset; float targetOffset = expectedWidth * (focusPoint / contentSize) - focusOffset;
d.currentZoom = newZoom; d.currentZoom = newZoom;
d.updateZoomedContentWidth(); d.updateZoomedContentWidth();
// Temporarily here to make sure ScrollTo gets the correct DrawSize for scrollable area. // Temporarily here to make sure ScrollTo gets the correct DrawSize for scrollable area.
// TODO: Make sure draw size gets invalidated properly on the framework side, and remove this once it is. // TODO: Make sure draw size gets invalidated properly on the framework side, and remove this once it is.
d.Invalidate(Invalidation.DrawSize); d.Invalidate(Invalidation.DrawSize);

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -9,6 +11,7 @@ using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
@ -27,10 +30,10 @@ namespace osu.Game.Screens.Edit.Setup
public IEnumerable<string> HandledExtensions => handledExtensions; public IEnumerable<string> HandledExtensions => handledExtensions;
private readonly Bindable<FileInfo> currentFile = new Bindable<FileInfo>(); private readonly Bindable<FileInfo?> currentFile = new Bindable<FileInfo?>();
[Resolved] [Resolved]
private OsuGameBase game { get; set; } private OsuGameBase game { get; set; } = null!;
public FileChooserLabelledTextBox(params string[] handledExtensions) public FileChooserLabelledTextBox(params string[] handledExtensions)
{ {
@ -45,7 +48,7 @@ namespace osu.Game.Screens.Edit.Setup
currentFile.BindValueChanged(onFileSelected); currentFile.BindValueChanged(onFileSelected);
} }
private void onFileSelected(ValueChangedEvent<FileInfo> file) private void onFileSelected(ValueChangedEvent<FileInfo?> file)
{ {
if (file.NewValue == null) if (file.NewValue == null)
return; return;
@ -65,14 +68,16 @@ namespace osu.Game.Screens.Edit.Setup
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
game.UnregisterImportHandler(this);
if (game.IsNotNull())
game.UnregisterImportHandler(this);
} }
public override Popover GetPopover() => new FileChooserPopover(handledExtensions, currentFile); public override Popover GetPopover() => new FileChooserPopover(handledExtensions, currentFile);
private class FileChooserPopover : OsuPopover private class FileChooserPopover : OsuPopover
{ {
public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo> currentFile) public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> currentFile)
{ {
Child = new Container Child = new Container
{ {

View File

@ -37,7 +37,7 @@ namespace osu.Game.Screens.Edit.Timing
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
Padding = new MarginPadding(10); Padding = new MarginPadding(10) { Bottom = 0 };
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {

View File

@ -38,6 +38,8 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved] [Resolved]
private OverlayColourProvider overlayColourProvider { get; set; } private OverlayColourProvider overlayColourProvider { get; set; }
public bool EnableClicking { get; set; } = true;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio) private void load(AudioManager audio)
{ {
@ -281,6 +283,9 @@ namespace osu.Game.Screens.Edit.Timing
Schedule(() => Schedule(() =>
{ {
if (!EnableClicking)
return;
var channel = clunk?.GetChannel(); var channel = clunk?.GetChannel();
if (channel != null) if (channel != null)

View File

@ -0,0 +1,92 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
namespace osu.Game.Screens.Edit.Timing
{
/// <summary>
/// Represents a component that provides the behaviour of triggering button clicks repeatedly while holding with mouse.
/// </summary>
public class RepeatingButtonBehaviour : Component
{
private const double initial_delay = 300;
private const double minimum_delay = 80;
private readonly Drawable button;
private Sample sample;
/// <summary>
/// An additive modifier for the frequency of the sample played on next actuation.
/// This can be adjusted during the button's <see cref="Drawable.OnClick"/> event to affect the repeat sample playback of that click.
/// </summary>
public double SampleFrequencyModifier { get; set; }
public RepeatingButtonBehaviour(Drawable button)
{
this.button = button;
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
sample = audio.Samples.Get(@"UI/notch-tick");
}
protected override bool OnMouseDown(MouseDownEvent e)
{
beginRepeat();
return true;
}
protected override void OnMouseUp(MouseUpEvent e)
{
adjustDelegate?.Cancel();
base.OnMouseUp(e);
}
private ScheduledDelegate adjustDelegate;
private double adjustDelay = initial_delay;
private void beginRepeat()
{
adjustDelegate?.Cancel();
adjustDelay = initial_delay;
adjustNext();
void adjustNext()
{
if (IsHovered)
{
button.TriggerClick();
adjustDelay = Math.Max(minimum_delay, adjustDelay * 0.9f);
var channel = sample?.GetChannel();
if (channel != null)
{
double repeatModifier = 0.05f * (Math.Abs(adjustDelay - initial_delay) / minimum_delay);
channel.Frequency.Value = 1 + repeatModifier + SampleFrequencyModifier;
channel.Play();
}
}
else
{
adjustDelay = initial_delay;
}
adjustDelegate = Scheduler.AddDelayed(adjustNext, adjustDelay);
}
}
}
}

View File

@ -77,7 +77,7 @@ namespace osu.Game.Screens.Edit.Timing
{ {
Flow = new FillFlowContainer Flow = new FillFlowContainer
{ {
Padding = new MarginPadding(20), Padding = new MarginPadding(10) { Top = 0 },
Spacing = new Vector2(20), Spacing = new Vector2(20),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,

View File

@ -0,0 +1,418 @@
// 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 enable
using System;
using System.Collections.Generic;
using System.Linq;
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.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Screens.Edit.Timing
{
internal class TapButton : CircularContainer, IKeyBindingHandler<GlobalAction>
{
public const float SIZE = 140;
public readonly BindableBool IsHandlingTapping = new BindableBool();
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[Resolved(canBeNull: true)]
private Bindable<ControlPointGroup>? selectedGroup { get; set; }
[Resolved(canBeNull: true)]
private IBeatSyncProvider? beatSyncSource { get; set; }
private Circle hoverLayer = null!;
private CircularContainer innerCircle = null!;
private Box innerCircleHighlight = null!;
private int currentLight;
private Container scaleContainer = null!;
private Container lights = null!;
private Container lightsGlow = null!;
private OsuSpriteText bpmText = null!;
private Container textContainer = null!;
private bool grabbedMouseDown;
private ScheduledDelegate? resetDelegate;
private const int light_count = 8;
private const int initial_taps_to_ignore = 4;
private const int max_taps_to_consider = 128;
private const double transition_length = 500;
private const float angular_light_gap = 0.007f;
private readonly List<double> tapTimings = new List<double>();
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(SIZE);
const float ring_width = 22;
const float light_padding = 3;
InternalChild = scaleContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4
},
lights = new Container
{
RelativeSizeAxes = Axes.Both,
},
new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Name = @"outer masking",
Masking = true,
BorderThickness = light_padding,
BorderColour = colourProvider.Background4,
Children = new Drawable[]
{
new Box
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true,
},
}
},
new Circle
{
Name = @"inner masking",
Size = new Vector2(SIZE - ring_width * 2 + light_padding * 2),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = colourProvider.Background4,
},
lightsGlow = new Container
{
RelativeSizeAxes = Axes.Both,
},
innerCircle = new CircularContainer
{
Size = new Vector2(SIZE - ring_width * 2),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Masking = true,
Children = new Drawable[]
{
new Box
{
Colour = colourProvider.Background2,
RelativeSizeAxes = Axes.Both,
},
innerCircleHighlight = new Box
{
Colour = colourProvider.Colour3,
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Alpha = 0,
},
textContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background1,
Children = new Drawable[]
{
new OsuSpriteText
{
Font = OsuFont.Torus.With(size: 34, weight: FontWeight.SemiBold),
Anchor = Anchor.Centre,
Origin = Anchor.BottomCentre,
Y = 5,
Text = "Tap",
},
bpmText = new OsuSpriteText
{
Font = OsuFont.Torus.With(size: 23, weight: FontWeight.Regular),
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
Y = -1,
},
}
},
hoverLayer = new Circle
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background1.Opacity(0.3f),
Blending = BlendingParameters.Additive,
Alpha = 0,
},
}
},
}
};
for (int i = 0; i < light_count; i++)
{
var light = new Light
{
Rotation = (i + 1) * (360f / light_count) + 360 * angular_light_gap / 2,
};
lights.Add(light);
lightsGlow.Add(light.Glow.CreateProxy());
}
reset();
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
hoverLayer.ReceivePositionalInputAt(screenSpacePos);
private ColourInfo textColour
{
get
{
if (grabbedMouseDown)
return colourProvider.Background4;
if (IsHovered)
return colourProvider.Content2;
return colourProvider.Background1;
}
}
protected override bool OnHover(HoverEvent e)
{
hoverLayer.FadeIn(transition_length, Easing.OutQuint);
textContainer.FadeColour(textColour, transition_length, Easing.OutQuint);
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
hoverLayer.FadeOut(transition_length, Easing.OutQuint);
textContainer.FadeColour(textColour, transition_length, Easing.OutQuint);
base.OnHoverLost(e);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
const double in_duration = 100;
grabbedMouseDown = true;
IsHandlingTapping.Value = true;
resetDelegate?.Cancel();
handleTap();
textContainer.FadeColour(textColour, in_duration, Easing.OutQuint);
scaleContainer.ScaleTo(0.99f, in_duration, Easing.OutQuint);
innerCircle.ScaleTo(0.96f, in_duration, Easing.OutQuint);
innerCircleHighlight
.FadeIn(50, Easing.OutQuint)
.FlashColour(Color4.White, 1000, Easing.OutQuint);
lights[currentLight % light_count].Hide();
lights[(currentLight + light_count / 2) % light_count].Hide();
currentLight++;
lights[currentLight % light_count].Show();
lights[(currentLight + light_count / 2) % light_count].Show();
return true;
}
protected override void OnMouseUp(MouseUpEvent e)
{
const double out_duration = 800;
grabbedMouseDown = false;
textContainer.FadeColour(textColour, out_duration, Easing.OutQuint);
scaleContainer.ScaleTo(1, out_duration, Easing.OutQuint);
innerCircle.ScaleTo(1, out_duration, Easing.OutQuint);
innerCircleHighlight.FadeOut(out_duration, Easing.OutQuint);
resetDelegate = Scheduler.AddDelayed(reset, 1000);
base.OnMouseUp(e);
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Action == GlobalAction.EditorTapForBPM && !e.Repeat)
{
// Direct through mouse handling to achieve animation
OnMouseDown(new MouseDownEvent(e.CurrentState, MouseButton.Left));
return true;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
if (e.Action == GlobalAction.EditorTapForBPM)
OnMouseUp(new MouseUpEvent(e.CurrentState, MouseButton.Left));
}
private void handleTap()
{
tapTimings.Add(Clock.CurrentTime);
if (tapTimings.Count > initial_taps_to_ignore + max_taps_to_consider)
tapTimings.RemoveAt(0);
if (tapTimings.Count < initial_taps_to_ignore * 2)
{
bpmText.Text = new string('.', tapTimings.Count);
return;
}
double averageBeatLength = (tapTimings.Last() - tapTimings.Skip(initial_taps_to_ignore).First()) / (tapTimings.Count - initial_taps_to_ignore - 1);
double clockRate = beatSyncSource?.Clock?.Rate ?? 1;
double bpm = Math.Round(60000 / averageBeatLength / clockRate);
bpmText.Text = $"{bpm} BPM";
var timingPoint = selectedGroup?.Value.ControlPoints.OfType<TimingControlPoint>().FirstOrDefault();
if (timingPoint != null)
{
// Intentionally use the rounded BPM here.
timingPoint.BeatLength = 60000 / bpm;
}
}
private void reset()
{
bpmText.FadeOut(transition_length, Easing.OutQuint);
using (BeginDelayedSequence(tapTimings.Count > 0 ? transition_length : 0))
{
Schedule(() => bpmText.Text = "the beat!");
bpmText.FadeIn(800, Easing.OutQuint);
}
foreach (var light in lights)
light.Hide();
tapTimings.Clear();
currentLight = 0;
IsHandlingTapping.Value = false;
}
private class Light : CompositeDrawable
{
public Drawable Glow { get; private set; } = null!;
private Container fillContent = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.Both;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Size = new Vector2(0.98f); // Avoid bleed into masking edge.
InternalChildren = new Drawable[]
{
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Current = { Value = 1f / light_count - angular_light_gap },
Colour = colourProvider.Background2,
},
fillContent = new Container
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Colour = colourProvider.Colour1,
Children = new[]
{
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Current = { Value = 1f / light_count - angular_light_gap },
Blending = BlendingParameters.Additive
},
// Please do not try and make sense of this.
// Getting the visual effect I was going for relies on what I can only imagine is broken implementation
// of `PadExtent`. If that's ever fixed in the future this will likely need to be adjusted.
Glow = new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Current = { Value = 1f / light_count - 0.01f },
Blending = BlendingParameters.Additive
}.WithEffect(new GlowEffect
{
Colour = colourProvider.Colour1.Opacity(0.4f),
BlurSigma = new Vector2(9f),
Strength = 10,
PadExtent = true
}),
}
},
};
}
public override void Show()
{
fillContent
.FadeIn(50, Easing.OutQuint)
.FlashColour(Color4.White, 1000, Easing.OutQuint);
}
public override void Hide()
{
fillContent
.FadeOut(300, Easing.OutQuint);
}
}
}
}

View File

@ -1,33 +1,45 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays; using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.Edit.Timing namespace osu.Game.Screens.Edit.Timing
{ {
public class TapTimingControl : CompositeDrawable public class TapTimingControl : CompositeDrawable
{ {
[Resolved] [Resolved]
private EditorClock editorClock { get; set; } private EditorClock editorClock { get; set; } = null!;
[Resolved] [Resolved]
private EditorBeatmap beatmap { get; set; } private EditorBeatmap beatmap { get; set; } = null!;
[Resolved] [Resolved]
private Bindable<ControlPointGroup> selectedGroup { get; set; } private Bindable<ControlPointGroup> selectedGroup { get; set; } = null!;
private readonly BindableBool isHandlingTapping = new BindableBool();
private MetronomeDisplay metronome = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, OsuColour colours) private void load(OverlayColourProvider colourProvider, OsuColour colours)
{ {
const float padding = 10;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
@ -48,8 +60,8 @@ namespace osu.Game.Screens.Edit.Timing
RowDimensions = new[] RowDimensions = new[]
{ {
new Dimension(GridSizeMode.Absolute, 200), new Dimension(GridSizeMode.Absolute, 200),
new Dimension(GridSizeMode.Absolute, 60), new Dimension(GridSizeMode.Absolute, 50),
new Dimension(GridSizeMode.Absolute, 60), new Dimension(GridSizeMode.Absolute, TapButton.SIZE + padding),
}, },
Content = new[] Content = new[]
{ {
@ -58,6 +70,7 @@ namespace osu.Game.Screens.Edit.Timing
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(padding),
Children = new Drawable[] Children = new Drawable[]
{ {
new GridContainer new GridContainer
@ -72,7 +85,7 @@ namespace osu.Game.Screens.Edit.Timing
{ {
new Drawable[] new Drawable[]
{ {
new MetronomeDisplay metronome = new MetronomeDisplay
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
@ -89,15 +102,14 @@ namespace osu.Game.Screens.Edit.Timing
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(10), Padding = new MarginPadding { Bottom = padding, Horizontal = padding },
Children = new Drawable[] Children = new Drawable[]
{ {
new TimingAdjustButton(1) new TimingAdjustButton(1)
{ {
Text = "Offset", Text = "Offset",
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.Both,
Width = 0.48f, Size = new Vector2(0.48f, 1),
Height = 50,
Action = adjustOffset, Action = adjustOffset,
}, },
new TimingAdjustButton(0.1) new TimingAdjustButton(0.1)
@ -105,9 +117,8 @@ namespace osu.Game.Screens.Edit.Timing
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
Text = "BPM", Text = "BPM",
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.Both,
Width = 0.48f, Size = new Vector2(0.48f, 1),
Height = 50,
Action = adjustBpm, Action = adjustBpm,
} }
} }
@ -118,33 +129,70 @@ namespace osu.Game.Screens.Edit.Timing
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(10), Padding = new MarginPadding { Bottom = padding, Horizontal = padding },
Children = new Drawable[] Children = new Drawable[]
{ {
new RoundedButton new Container
{ {
Text = "Reset", RelativeSizeAxes = Axes.Y,
BackgroundColour = colours.Pink, Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.X, Origin = Anchor.CentreRight,
Width = 0.3f, Height = 0.98f,
Action = reset, Width = TapButton.SIZE / 1.3f,
Masking = true,
CornerRadius = 15,
Children = new Drawable[]
{
new InlineButton(FontAwesome.Solid.Stop, Anchor.TopLeft)
{
BackgroundColour = colourProvider.Background1,
RelativeSizeAxes = Axes.Both,
Height = 0.49f,
Action = reset,
},
new InlineButton(FontAwesome.Solid.Play, Anchor.BottomLeft)
{
BackgroundColour = colourProvider.Background1,
RelativeSizeAxes = Axes.Both,
Height = 0.49f,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Action = start,
},
},
}, },
new RoundedButton new TapButton
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.Centre,
Origin = Anchor.TopRight, Origin = Anchor.Centre,
Text = "Play from start", IsHandlingTapping = { BindTarget = isHandlingTapping }
RelativeSizeAxes = Axes.X,
BackgroundColour = colourProvider.Background1,
Width = 0.68f,
Action = tap,
} }
} }
}, },
} },
} }
}, },
}; };
isHandlingTapping.BindValueChanged(handling =>
{
metronome.EnableClicking = !handling.NewValue;
if (handling.NewValue)
start();
}, true);
}
private void start()
{
editorClock.Seek(selectedGroup.Value.Time);
editorClock.Start();
}
private void reset()
{
editorClock.Stop();
editorClock.Seek(selectedGroup.Value.Time);
} }
private void adjustOffset(double adjust) private void adjustOffset(double adjust)
@ -176,16 +224,66 @@ namespace osu.Game.Screens.Edit.Timing
timing.BeatLength = 60000 / (timing.BPM + adjust); timing.BeatLength = 60000 / (timing.BPM + adjust);
} }
private void tap() private class InlineButton : OsuButton
{ {
editorClock.Seek(selectedGroup.Value.Time); private readonly IconUsage icon;
editorClock.Start(); private readonly Anchor anchor;
}
private void reset() private SpriteIcon spriteIcon = null!;
{
editorClock.Stop(); [Resolved]
editorClock.Seek(selectedGroup.Value.Time); private OverlayColourProvider colourProvider { get; set; } = null!;
public InlineButton(IconUsage icon, Anchor anchor)
{
this.icon = icon;
this.anchor = anchor;
}
protected override void LoadComplete()
{
base.LoadComplete();
Content.CornerRadius = 0;
Content.Masking = false;
BackgroundColour = colourProvider.Background2;
Content.Add(new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(15),
Children = new Drawable[]
{
spriteIcon = new SpriteIcon
{
Icon = icon,
Size = new Vector2(22),
Anchor = anchor,
Origin = anchor,
Colour = colourProvider.Background1,
},
}
});
}
protected override bool OnMouseDown(MouseDownEvent e)
{
// scale looks bad so don't call base.
return false;
}
protected override bool OnHover(HoverEvent e)
{
spriteIcon.FadeColour(colourProvider.Content2, 200, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
spriteIcon.FadeColour(colourProvider.Background1, 200, Easing.OutQuint);
base.OnHoverLost(e);
}
} }
} }
} }

View File

@ -4,14 +4,11 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Threading;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -26,32 +23,24 @@ namespace osu.Game.Screens.Edit.Timing
public Action<double> Action; public Action<double> Action;
private readonly double adjustAmount; private readonly double adjustAmount;
private ScheduledDelegate adjustDelegate;
private const int max_multiplier = 10; private const int max_multiplier = 10;
private const int adjust_levels = 4; private const int adjust_levels = 4;
private const double initial_delay = 300;
private const double minimum_delay = 80;
public Container Content { get; set; } public Container Content { get; set; }
private double adjustDelay = initial_delay;
private readonly Box background; private readonly Box background;
private readonly OsuSpriteText text; private readonly OsuSpriteText text;
private Sample sample;
public LocalisableString Text public LocalisableString Text
{ {
get => text.Text; get => text.Text;
set => text.Text = value; set => text.Text = value;
} }
private readonly RepeatingButtonBehaviour repeatBehaviour;
[Resolved] [Resolved]
private OverlayColourProvider colourProvider { get; set; } private OverlayColourProvider colourProvider { get; set; }
@ -82,13 +71,13 @@ namespace osu.Game.Screens.Edit.Timing
} }
} }
}); });
AddInternal(repeatBehaviour = new RepeatingButtonBehaviour(this));
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio) private void load()
{ {
sample = audio.Samples.Get(@"UI/notch-tick");
background.Colour = colourProvider.Background3; background.Colour = colourProvider.Background3;
for (int i = 1; i <= adjust_levels; i++) for (int i = 1; i <= adjust_levels; i++)
@ -98,57 +87,22 @@ namespace osu.Game.Screens.Edit.Timing
} }
} }
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnHover(HoverEvent e) => true;
protected override bool OnClick(ClickEvent e)
{ {
beginRepeat(); var hoveredBox = Content.OfType<IncrementBox>().FirstOrDefault(d => d.IsHovered);
if (hoveredBox == null)
return false;
Action(adjustAmount * hoveredBox.Multiplier);
hoveredBox.Flash();
repeatBehaviour.SampleFrequencyModifier = (hoveredBox.Multiplier / max_multiplier) * 0.2;
return true; return true;
} }
protected override void OnMouseUp(MouseUpEvent e)
{
adjustDelegate?.Cancel();
base.OnMouseUp(e);
}
private void beginRepeat()
{
adjustDelegate?.Cancel();
adjustDelay = initial_delay;
adjustNext();
void adjustNext()
{
var hoveredBox = Content.OfType<IncrementBox>().FirstOrDefault(d => d.IsHovered);
if (hoveredBox != null)
{
Action(adjustAmount * hoveredBox.Multiplier);
adjustDelay = Math.Max(minimum_delay, adjustDelay * 0.9f);
hoveredBox.Flash();
var channel = sample?.GetChannel();
if (channel != null)
{
double repeatModifier = 0.05f * (Math.Abs(adjustDelay - initial_delay) / minimum_delay);
double multiplierModifier = (hoveredBox.Multiplier / max_multiplier) * 0.2f;
channel.Frequency.Value = 1 + multiplierModifier + repeatModifier;
channel.Play();
}
}
else
{
adjustDelay = initial_delay;
}
adjustDelegate = Scheduler.AddDelayed(adjustNext, adjustDelay);
}
}
private class IncrementBox : CompositeDrawable private class IncrementBox : CompositeDrawable
{ {
public readonly float Multiplier; public readonly float Multiplier;
@ -187,7 +141,7 @@ namespace osu.Game.Screens.Edit.Timing
Origin = direction, Origin = direction,
Font = OsuFont.Default.With(size: 10, weight: FontWeight.Bold), Font = OsuFont.Default.With(size: 10, weight: FontWeight.Bold),
Text = $"{(index > 0 ? "+" : "-")}{Math.Abs(Multiplier * amount)}", Text = $"{(index > 0 ? "+" : "-")}{Math.Abs(Multiplier * amount)}",
Padding = new MarginPadding(5), Padding = new MarginPadding(2),
Alpha = 0, Alpha = 0,
} }
}; };

View File

@ -1,12 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -51,6 +53,8 @@ namespace osu.Game.Screens.Edit.Timing
private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>(); private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
private RoundedButton addButton;
[Resolved] [Resolved]
private EditorClock clock { get; set; } private EditorClock clock { get; set; }
@ -105,9 +109,8 @@ namespace osu.Game.Screens.Edit.Timing
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
}, },
new RoundedButton addButton = new RoundedButton
{ {
Text = "+ Add at current time",
Action = addNew, Action = addNew,
Size = new Vector2(160, 30), Size = new Vector2(160, 30),
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
@ -122,7 +125,14 @@ namespace osu.Game.Screens.Edit.Timing
{ {
base.LoadComplete(); base.LoadComplete();
selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true); selectedGroup.BindValueChanged(selected =>
{
deleteButton.Enabled.Value = selected.NewValue != null;
addButton.Text = selected.NewValue != null
? "+ Clone to current time"
: "+ Add at current time";
}, true);
controlPointGroups.BindTo(Beatmap.ControlPointInfo.Groups); controlPointGroups.BindTo(Beatmap.ControlPointInfo.Groups);
controlPointGroups.BindCollectionChanged((sender, args) => controlPointGroups.BindCollectionChanged((sender, args) =>
@ -132,13 +142,23 @@ namespace osu.Game.Screens.Edit.Timing
}, true); }, true);
} }
protected override bool OnClick(ClickEvent e)
{
selectedGroup.Value = null;
return true;
}
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
trackActivePoint(); trackActivePoint();
addButton.Enabled.Value = clock.CurrentTimeAccurate != selectedGroup.Value?.Time;
} }
private Type trackedType;
/// <summary> /// <summary>
/// Given the user has selected a control point group, we want to track any group which is /// Given the user has selected a control point group, we want to track any group which is
/// active at the current point in time which matches the type the user has selected. /// active at the current point in time which matches the type the user has selected.
@ -149,16 +169,27 @@ namespace osu.Game.Screens.Edit.Timing
private void trackActivePoint() private void trackActivePoint()
{ {
// For simplicity only match on the first type of the active control point. // For simplicity only match on the first type of the active control point.
var selectedPointType = selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType(); if (selectedGroup.Value == null)
trackedType = null;
else
{
// If the selected group only has one control point, update the tracking type.
if (selectedGroup.Value.ControlPoints.Count == 1)
trackedType = selectedGroup.Value?.ControlPoints.Single().GetType();
// If the selected group has more than one control point, choose the first as the tracking type
// if we don't already have a singular tracked type.
else if (trackedType == null)
trackedType = selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType();
}
if (selectedPointType != null) if (trackedType != null)
{ {
// We don't have an efficient way of looking up groups currently, only individual point types. // We don't have an efficient way of looking up groups currently, only individual point types.
// To improve the efficiency of this in the future, we should reconsider the overall structure of ControlPointInfo. // To improve the efficiency of this in the future, we should reconsider the overall structure of ControlPointInfo.
// Find the next group which has the same type as the selected one. // Find the next group which has the same type as the selected one.
var found = Beatmap.ControlPointInfo.Groups var found = Beatmap.ControlPointInfo.Groups
.Where(g => g.ControlPoints.Any(cp => cp.GetType() == selectedPointType)) .Where(g => g.ControlPoints.Any(cp => cp.GetType() == trackedType))
.LastOrDefault(g => g.Time <= clock.CurrentTimeAccurate); .LastOrDefault(g => g.Time <= clock.CurrentTimeAccurate);
if (found != null) if (found != null)
@ -189,7 +220,7 @@ namespace osu.Game.Screens.Edit.Timing
// Try and create matching types from the currently selected control point. // Try and create matching types from the currently selected control point.
var selected = selectedGroup.Value; var selected = selectedGroup.Value;
if (selected != null) if (selected != null && selected != group)
{ {
foreach (var controlPoint in selected.ControlPoints) foreach (var controlPoint in selected.ControlPoints)
{ {

View File

@ -55,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
private void load(AudioManager audio) private void load(AudioManager audio)
{ {
sampleSelect = audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select"); sampleSelect = audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select");
sampleJoin = audio.Samples.Get($@"UI/{HoverSampleSet.Submit.GetDescription()}-select"); sampleJoin = audio.Samples.Get($@"UI/{HoverSampleSet.Button.GetDescription()}-select");
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {

View File

@ -8,6 +8,7 @@ using System.Threading;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -244,7 +245,7 @@ namespace osu.Game.Screens.Select.Carousel
} }
if (hideRequested != null) if (hideRequested != null)
items.Add(new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested(beatmapInfo))); items.Add(new OsuMenuItem(CommonStrings.ButtonsHide.ToSentence(), MenuItemType.Destructive, () => hideRequested(beatmapInfo)));
return items.ToArray(); return items.ToArray();
} }

View File

@ -15,7 +15,7 @@ namespace osu.Game.Screens.Select.Filter
[Description("Author")] [Description("Author")]
Author, Author,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatsBpm))] [LocalisableDescription(typeof(SortStrings), nameof(SortStrings.ArtistTracksBpm))]
BPM, BPM,
[Description("Date Added")] [Description("Date Added")]
@ -28,10 +28,10 @@ namespace osu.Game.Screens.Select.Filter
Length, Length,
// todo: pending support (https://github.com/ppy/osu/issues/4917) // todo: pending support (https://github.com/ppy/osu/issues/4917)
// [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchFiltersRank))] // [Description("Rank Achieved")]
// RankAchieved, // RankAchieved,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoSource))] [Description("Source")]
Source, Source,
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchSortingTitle))] [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchSortingTitle))]

View File

@ -66,7 +66,7 @@ namespace osu.Game.Screens.Select
private readonly Box light; private readonly Box light;
public FooterButton() public FooterButton()
: base(HoverSampleSet.Button) : base(HoverSampleSet.Toolbar)
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
Shear = SHEAR; Shear = SHEAR;

View File

@ -77,7 +77,7 @@ namespace osu.Game.Screens.Select.Options
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
public BeatmapOptionsButton() public BeatmapOptionsButton()
: base(HoverSampleSet.Submit) : base(HoverSampleSet.Button)
{ {
Width = width; Width = width;
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;

View File

@ -23,7 +23,7 @@ namespace osu.Game.Skinning
/// The <see cref="ISkin"/> which is being transformed. /// The <see cref="ISkin"/> which is being transformed.
/// </summary> /// </summary>
[NotNull] [NotNull]
protected internal ISkin Skin { get; } public ISkin Skin { get; }
protected LegacySkinTransformer([NotNull] ISkin skin) protected LegacySkinTransformer([NotNull] ISkin skin)
{ {

View File

@ -3,7 +3,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using Newtonsoft.Json;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
@ -44,9 +46,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay
switch (request) switch (request)
{ {
case CreateRoomRequest createRoomRequest: case CreateRoomRequest createRoomRequest:
var apiRoom = new Room(); var apiRoom = cloneRoom(createRoomRequest.Room);
apiRoom.CopyFrom(createRoomRequest.Room);
// Passwords are explicitly not copied between rooms. // Passwords are explicitly not copied between rooms.
apiRoom.HasPassword.Value = !string.IsNullOrEmpty(createRoomRequest.Room.Password.Value); apiRoom.HasPassword.Value = !string.IsNullOrEmpty(createRoomRequest.Room.Password.Value);
@ -178,12 +178,31 @@ namespace osu.Game.Tests.Visual.OnlinePlay
private Room createResponseRoom(Room room, bool withParticipants) private Room createResponseRoom(Room room, bool withParticipants)
{ {
var responseRoom = new Room(); var responseRoom = cloneRoom(room);
responseRoom.CopyFrom(room);
// Password is hidden from the response, and is only propagated via HasPassword.
bool hadPassword = responseRoom.HasPassword.Value;
responseRoom.Password.Value = null; responseRoom.Password.Value = null;
responseRoom.HasPassword.Value = hadPassword;
if (!withParticipants) if (!withParticipants)
responseRoom.RecentParticipants.Clear(); responseRoom.RecentParticipants.Clear();
return responseRoom; return responseRoom;
} }
private Room cloneRoom(Room source)
{
var result = JsonConvert.DeserializeObject<Room>(JsonConvert.SerializeObject(source));
Debug.Assert(result != null);
// Playlist item IDs aren't serialised.
if (source.CurrentPlaylistItem.Value != null)
result.CurrentPlaylistItem.Value.ID = source.CurrentPlaylistItem.Value.ID;
for (int i = 0; i < source.Playlist.Count; i++)
result.Playlist[i].ID = source.Playlist[i].ID;
return result;
}
} }
} }

View File

@ -7,7 +7,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Users.Drawables namespace osu.Game.Users.Drawables
@ -74,11 +73,6 @@ namespace osu.Game.Users.Drawables
{ {
private LocalisableString tooltip = default_tooltip_text; private LocalisableString tooltip = default_tooltip_text;
public ClickableArea()
: base(HoverSampleSet.Submit)
{
}
public override LocalisableString TooltipText public override LocalisableString TooltipText
{ {
get => Enabled.Value ? tooltip : default; get => Enabled.Value ? tooltip : default;

View File

@ -49,7 +49,7 @@ namespace osu.Game.Users.Drawables
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },
new HoverClickSounds(HoverSampleSet.Submit) new HoverClickSounds()
} }
}; };
} }

View File

@ -32,7 +32,7 @@ namespace osu.Game.Users
protected Drawable Background { get; private set; } protected Drawable Background { get; private set; }
protected UserPanel(APIUser user) protected UserPanel(APIUser user)
: base(HoverSampleSet.Submit) : base(HoverSampleSet.Button)
{ {
if (user == null) if (user == null)
throw new ArgumentNullException(nameof(user)); throw new ArgumentNullException(nameof(user));

View File

@ -43,7 +43,7 @@ namespace osu.Game.Utils
sentrySession = SentrySdk.Init(options => sentrySession = SentrySdk.Init(options =>
{ {
// Not setting the dsn will completely disable sentry. // Not setting the dsn will completely disable sentry.
if (game.IsDeployedBuild) if (game.IsDeployedBuild && game.CreateEndpoints().WebsiteRootUrl.EndsWith(@".ppy.sh", StringComparison.Ordinal))
options.Dsn = "https://ad9f78529cef40ac874afb95a9aca04e@sentry.ppy.sh/2"; options.Dsn = "https://ad9f78529cef40ac874afb95a9aca04e@sentry.ppy.sh/2";
options.AutoSessionTracking = true; options.AutoSessionTracking = true;
@ -159,6 +159,7 @@ namespace osu.Game.Utils
Game = game.Clock.CurrentTime, Game = game.Clock.CurrentTime,
}; };
scope.SetTag(@"beatmap", $"{beatmap.OnlineID}");
scope.SetTag(@"ruleset", ruleset.ShortName); scope.SetTag(@"ruleset", ruleset.ShortName);
scope.SetTag(@"os", $"{RuntimeInfo.OS} ({Environment.OSVersion})"); scope.SetTag(@"os", $"{RuntimeInfo.OS} ({Environment.OSVersion})");
scope.SetTag(@"processor count", Environment.ProcessorCount.ToString()); scope.SetTag(@"processor count", Environment.ProcessorCount.ToString());

View File

@ -35,8 +35,8 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.11.2" /> <PackageReference Include="Realm" Version="10.14.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.530.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.605.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.527.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.527.0" />
<PackageReference Include="Sentry" Version="3.17.1" /> <PackageReference Include="Sentry" Version="3.17.1" />
<PackageReference Include="SharpCompress" Version="0.31.0" /> <PackageReference Include="SharpCompress" Version="0.31.0" />

View File

@ -61,7 +61,7 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.530.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2022.605.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.527.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.527.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
@ -84,11 +84,11 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2022.530.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.605.0" />
<PackageReference Include="SharpCompress" Version="0.31.0" /> <PackageReference Include="SharpCompress" Version="0.31.0" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2022.429.0" ExcludeAssets="all" /> <PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2022.429.0" ExcludeAssets="all" />
<PackageReference Include="Realm" Version="10.11.2" /> <PackageReference Include="Realm" Version="10.14.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>