mirror of
https://github.com/ppy/osu.git
synced 2024-11-13 21:27:54 +08:00
Merge branch 'ppy:master' into colour-rework
This commit is contained in:
commit
29b259ccac
@ -52,10 +52,10 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<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 Label="Transitive Dependencies">
|
||||
<!-- 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>
|
||||
</Project>
|
||||
|
@ -14,11 +14,11 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
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)
|
||||
=> 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)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime());
|
||||
|
||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
foreach (var v in base.ToDatabaseAttributes())
|
||||
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_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||
yield return (ATTRIB_ID_SCORE_MULTIPLIER, ScoreMultiplier);
|
||||
@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
base.FromDatabaseAttributes(values);
|
||||
|
||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||
ScoreMultiplier = values[ATTRIB_ID_SCORE_MULTIPLIER];
|
||||
|
@ -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.
|
||||
GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
|
||||
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)
|
||||
{
|
||||
var sortedObjects = beatmap.HitObjects.ToArray();
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -710,7 +710,7 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||
|
||||
realm.Realm.Write(() =>
|
||||
await realm.Realm.WriteAsync(() =>
|
||||
{
|
||||
foreach (var b in imported.Beatmaps)
|
||||
b.OnlineID = -1;
|
||||
|
@ -61,13 +61,13 @@ namespace osu.Game.Tests.Gameplay
|
||||
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
||||
|
||||
// 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.JudgedHits, Is.EqualTo(1));
|
||||
|
||||
// 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)
|
||||
});
|
||||
@ -76,7 +76,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
||||
|
||||
// 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)
|
||||
});
|
||||
|
48
osu.Game.Tests/Visual/Editing/TestSceneTapButton.cs
Normal file
48
osu.Game.Tests/Visual/Editing/TestSceneTapButton.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Edit;
|
||||
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}";
|
||||
}
|
||||
|
||||
[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]
|
||||
public void TestBasic()
|
||||
{
|
||||
@ -115,7 +87,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
AddStep("click tap button", () =>
|
||||
{
|
||||
control.ChildrenOfType<RoundedButton>()
|
||||
control.ChildrenOfType<OsuButton>()
|
||||
.Last()
|
||||
.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)
|
||||
{
|
||||
Beatmap.Disabled = false;
|
||||
|
48
osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs
Normal file
48
osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -44,7 +44,12 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = OsuColour.Gray(30)
|
||||
},
|
||||
scrollContainer = new ZoomableScrollContainer { RelativeSizeAxes = Axes.Both }
|
||||
scrollContainer = new ZoomableScrollContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
}
|
||||
},
|
||||
new MenuCursor()
|
||||
@ -62,7 +67,15 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Test]
|
||||
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]
|
||||
|
@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
protected readonly BeatmapDownloadTracker DownloadTracker;
|
||||
|
||||
protected BeatmapCard(APIBeatmapSet beatmapSet, bool allowExpansion = true)
|
||||
: base(HoverSampleSet.Submit)
|
||||
: base(HoverSampleSet.Button)
|
||||
{
|
||||
Expanded = new BindableBool { Disabled = !allowExpansion };
|
||||
|
||||
|
@ -6,7 +6,6 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps.Drawables.Cards.Statistics;
|
||||
using osu.Game.Graphics;
|
||||
@ -245,10 +244,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
});
|
||||
|
||||
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)
|
||||
leftIconArea.Add(new IconPill(FontAwesome.Solid.Image) { IconSize = new Vector2(20) });
|
||||
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(20) });
|
||||
|
||||
if (BeatmapSet.FeaturedInSpotlight)
|
||||
{
|
||||
|
@ -7,7 +7,6 @@ using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps.Drawables.Cards.Statistics;
|
||||
using osu.Game.Graphics;
|
||||
@ -226,10 +225,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
});
|
||||
|
||||
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)
|
||||
leftIconArea.Add(new IconPill(FontAwesome.Solid.Image) { IconSize = new Vector2(20) });
|
||||
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(20) });
|
||||
|
||||
if (BeatmapSet.FeaturedInSpotlight)
|
||||
{
|
||||
|
@ -3,14 +3,16 @@
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
{
|
||||
public class IconPill : CircularContainer
|
||||
public abstract class IconPill : CircularContainer, IHasTooltip
|
||||
{
|
||||
public Vector2 IconSize
|
||||
{
|
||||
@ -20,7 +22,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
|
||||
private readonly Container iconContainer;
|
||||
|
||||
public IconPill(IconUsage icon)
|
||||
protected IconPill(IconUsage icon)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Masking = true;
|
||||
@ -47,5 +49,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public abstract LocalisableString TooltipText { get; }
|
||||
}
|
||||
}
|
||||
|
19
osu.Game/Beatmaps/Drawables/Cards/StoryboardIconPill.cs
Normal file
19
osu.Game/Beatmaps/Drawables/Cards/StoryboardIconPill.cs
Normal 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;
|
||||
}
|
||||
}
|
19
osu.Game/Beatmaps/Drawables/Cards/VideoIconPill.cs
Normal file
19
osu.Game/Beatmaps/Drawables/Cards/VideoIconPill.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -392,7 +392,7 @@ namespace osu.Game.Database
|
||||
{
|
||||
total_writes_async.Value++;
|
||||
using (var realm = getRealmInstance())
|
||||
await realm.WriteAsync(action);
|
||||
await realm.WriteAsync(() => action(realm));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
Size = TwoLayerButton.SIZE_EXTENDED;
|
||||
|
||||
Child = button = new TwoLayerButton(HoverSampleSet.Submit)
|
||||
Child = button = new TwoLayerButton
|
||||
{
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
|
@ -56,8 +56,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
private readonly SpriteText spriteText;
|
||||
private Vector2 hoverSpacing => new Vector2(3f, 0f);
|
||||
|
||||
public DialogButton()
|
||||
: base(HoverSampleSet.Submit)
|
||||
public DialogButton(HoverSampleSet sampleSet = HoverSampleSet.Button)
|
||||
: base(sampleSet)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
|
||||
|
@ -36,7 +36,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Icon = FontAwesome.Solid.ExternalLinkAlt,
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
new HoverClickSounds(HoverSampleSet.Submit)
|
||||
new HoverClickSounds()
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
@ -37,7 +38,10 @@ namespace osu.Game.Graphics.UserInterface
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
@ -10,9 +10,6 @@ namespace osu.Game.Graphics.UserInterface
|
||||
[Description("default")]
|
||||
Default,
|
||||
|
||||
[Description("submit")]
|
||||
Submit,
|
||||
|
||||
[Description("button")]
|
||||
Button,
|
||||
|
||||
|
@ -13,6 +13,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public class ShearedToggleButton : ShearedButton
|
||||
{
|
||||
private Sample? sampleClick;
|
||||
private Sample? sampleOff;
|
||||
private Sample? sampleOn;
|
||||
|
||||
@ -39,8 +40,9 @@ namespace osu.Game.Graphics.UserInterface
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
sampleOn = audio.Samples.Get(@"UI/check-on");
|
||||
sampleOff = audio.Samples.Get(@"UI/check-off");
|
||||
sampleClick = audio.Samples.Get(@"UI/default-select");
|
||||
sampleOn = audio.Samples.Get(@"UI/dropdown-open");
|
||||
sampleOff = audio.Samples.Get(@"UI/dropdown-close");
|
||||
}
|
||||
|
||||
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
|
||||
@ -67,6 +69,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
private void playSample()
|
||||
{
|
||||
sampleClick?.Play();
|
||||
|
||||
if (Active.Value)
|
||||
sampleOn?.Play();
|
||||
else
|
||||
|
@ -80,6 +80,7 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight),
|
||||
new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode),
|
||||
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.J }, GlobalAction.EditorFlipVertically),
|
||||
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))]
|
||||
DeselectAllMods,
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTapForBPM))]
|
||||
EditorTapForBPM,
|
||||
}
|
||||
}
|
||||
|
@ -174,6 +174,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
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>
|
||||
/// "Cycle grid display mode"
|
||||
/// </summary>
|
||||
|
@ -24,6 +24,11 @@ namespace osu.Game.Localisation
|
||||
/// </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.");
|
||||
|
||||
/// <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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,8 +121,16 @@ namespace osu.Game.Online.API
|
||||
|
||||
if (isFailing) return;
|
||||
|
||||
Logger.Log($@"Performing request {this}", LoggingTarget.Network);
|
||||
WebRequest.Perform();
|
||||
try
|
||||
{
|
||||
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;
|
||||
|
||||
|
@ -38,7 +38,6 @@ namespace osu.Game.Online.Chat
|
||||
}
|
||||
|
||||
public DrawableLinkCompiler(IEnumerable<Drawable> parts)
|
||||
: base(HoverSampleSet.Submit)
|
||||
{
|
||||
Parts = parts.ToList();
|
||||
}
|
||||
|
@ -197,6 +197,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
|
||||
APIRoom.Playlist.Clear();
|
||||
APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem));
|
||||
APIRoom.CurrentPlaylistItem.Value = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId);
|
||||
|
||||
Debug.Assert(LocalUser != null);
|
||||
addUserToAPIRoom(LocalUser);
|
||||
@ -737,6 +738,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
APIRoom.Type.Value = Room.Settings.MatchType;
|
||||
APIRoom.QueueMode.Value = Room.Settings.QueueMode;
|
||||
APIRoom.AutoStartDuration.Value = Room.Settings.AutoStartDuration;
|
||||
APIRoom.CurrentPlaylistItem.Value = APIRoom.Playlist.Single(item => item.ID == settings.PlaylistItemId);
|
||||
|
||||
RoomUpdated?.Invoke();
|
||||
}
|
||||
|
@ -61,7 +61,13 @@ namespace osu.Game.Online.Rooms
|
||||
/// Used for serialising to the API.
|
||||
/// </summary>
|
||||
[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>
|
||||
/// A beatmap representing this playlist item.
|
||||
|
@ -162,6 +162,13 @@ namespace osu.Game.Online.Rooms
|
||||
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)
|
||||
{
|
||||
RoomID.Value = other.RoomID.Value;
|
||||
|
@ -9,6 +9,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Humanizer;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
@ -658,11 +659,14 @@ namespace osu.Game
|
||||
}
|
||||
|
||||
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)
|
||||
{ FrameworkSetting.WindowMode, WindowMode.Fullscreen }
|
||||
// General expectation that osu! starts in fullscreen by default (also gives the most predictable performance).
|
||||
// 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()
|
||||
{
|
||||
|
@ -80,6 +80,9 @@ namespace osu.Game
|
||||
|
||||
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();
|
||||
|
||||
/// <summary>
|
||||
@ -268,7 +271,7 @@ namespace osu.Game
|
||||
dependencies.Cache(SkinManager = new SkinManager(Storage, realm, Host, Resources, Audio, Scheduler));
|
||||
dependencies.CacheAs<ISkinSource>(SkinManager);
|
||||
|
||||
EndpointConfiguration endpoints = UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
|
||||
EndpointConfiguration endpoints = CreateEndpoints();
|
||||
|
||||
MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl;
|
||||
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
@ -69,7 +70,7 @@ namespace osu.Game.Overlays.AccountCreation
|
||||
},
|
||||
usernameTextBox = new OsuTextBox
|
||||
{
|
||||
PlaceholderText = UsersStrings.LoginUsername,
|
||||
PlaceholderText = UsersStrings.LoginUsername.ToLower(),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
TabbableContentContainer = this
|
||||
},
|
||||
@ -91,7 +92,7 @@ namespace osu.Game.Overlays.AccountCreation
|
||||
},
|
||||
passwordTextBox = new OsuPasswordTextBox
|
||||
{
|
||||
PlaceholderText = "password",
|
||||
PlaceholderText = UsersStrings.LoginPassword.ToLower(),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
|
@ -40,7 +40,7 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular),
|
||||
Text = LabelFor(Value)
|
||||
},
|
||||
new HoverClickSounds()
|
||||
new HoverClickSounds(HoverSampleSet.TabSelect)
|
||||
});
|
||||
|
||||
Enabled.Value = true;
|
||||
|
@ -6,6 +6,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -16,6 +18,7 @@ using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Chat;
|
||||
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 bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); }
|
||||
|
||||
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds();
|
||||
|
||||
private Box hoverBox = null!;
|
||||
private SpriteIcon checkbox = null!;
|
||||
private OsuSpriteText channelText = null!;
|
||||
@ -46,14 +51,20 @@ namespace osu.Game.Overlays.Chat.Listing
|
||||
|
||||
private const float vertical_margin = 1.5f;
|
||||
|
||||
private Sample? sampleJoin;
|
||||
private Sample? sampleLeave;
|
||||
|
||||
public ChannelListingItem(Channel channel)
|
||||
{
|
||||
Channel = channel;
|
||||
}
|
||||
|
||||
[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;
|
||||
CornerRadius = 5;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
@ -156,7 +167,19 @@ namespace osu.Game.Overlays.Chat.Listing
|
||||
}
|
||||
}, 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)
|
||||
|
@ -14,7 +14,6 @@ using osu.Framework.Platform;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
|
||||
namespace osu.Game.Overlays.News
|
||||
@ -29,7 +28,6 @@ namespace osu.Game.Overlays.News
|
||||
private TextFlowContainer main;
|
||||
|
||||
public NewsCard(APINewsPost post)
|
||||
: base(HoverSampleSet.Submit)
|
||||
{
|
||||
this.post = post;
|
||||
|
||||
|
@ -18,7 +18,6 @@ using System.Diagnostics;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Overlays.News.Sidebar
|
||||
{
|
||||
@ -129,7 +128,6 @@ namespace osu.Game.Overlays.News.Sidebar
|
||||
private readonly APINewsPost post;
|
||||
|
||||
public PostButton(APINewsPost post)
|
||||
: base(HoverSampleSet.Submit)
|
||||
{
|
||||
this.post = post;
|
||||
|
||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
private Sample sampleOpen;
|
||||
private Sample sampleClose;
|
||||
|
||||
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds();
|
||||
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds();
|
||||
|
||||
public ExpandDetailsButton()
|
||||
{
|
||||
|
@ -6,7 +6,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Sections
|
||||
{
|
||||
@ -18,7 +17,6 @@ namespace osu.Game.Overlays.Profile.Sections
|
||||
private readonly IBeatmapInfo beatmapInfo;
|
||||
|
||||
protected BeatmapMetadataContainer(IBeatmapInfo beatmapInfo)
|
||||
: base(HoverSampleSet.Submit)
|
||||
{
|
||||
this.beatmapInfo = beatmapInfo;
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Configuration;
|
||||
@ -224,6 +225,16 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
|
||||
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)
|
||||
{
|
||||
windowModeDropdown.SetNoticeText(GraphicsSettingsStrings.NotFullscreenNote, true);
|
||||
|
@ -20,7 +20,6 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@ -83,7 +82,6 @@ namespace osu.Game.Overlays.Toolbar
|
||||
private RealmAccess realm { get; set; }
|
||||
|
||||
protected ToolbarButton()
|
||||
: base(HoverSampleSet.Toolbar)
|
||||
{
|
||||
Width = Toolbar.HEIGHT;
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
|
@ -11,7 +11,6 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -29,7 +28,6 @@ namespace osu.Game.Overlays.Toolbar
|
||||
private AnalogClockDisplay analog;
|
||||
|
||||
public ToolbarClock()
|
||||
: base(HoverSampleSet.Toolbar)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
AutoSizeAxes = Axes.X;
|
||||
|
@ -117,9 +117,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// <remarks>
|
||||
/// If the provided replay frame does not have any header information, this will be a noop.
|
||||
/// </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>
|
||||
public virtual void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame)
|
||||
public virtual void ResetFromReplayFrame(ReplayFrame frame)
|
||||
{
|
||||
if (frame.Header == null)
|
||||
return;
|
||||
|
@ -6,11 +6,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -88,17 +90,34 @@ namespace osu.Game.Rulesets.Scoring
|
||||
private readonly double accuracyPortion;
|
||||
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>
|
||||
/// The maximum achievable base score.
|
||||
/// Scoring values for the current play assuming all perfect hits.
|
||||
/// </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>
|
||||
/// The maximum number of basic (non-tick and non-bonus) hitobjects.
|
||||
/// Scoring values for the current play.
|
||||
/// </summary>
|
||||
private int maxBasicHitObjects;
|
||||
private ScoringValues currentScoringValues;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum <see cref="HitResult"/> of a basic (non-tick and non-bonus) hitobject.
|
||||
@ -106,9 +125,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
private HitResult? maxBasicResult;
|
||||
|
||||
private double rollingMaxBaseScore;
|
||||
private double baseScore;
|
||||
private int basicHitObjects;
|
||||
private bool beatmapApplied;
|
||||
|
||||
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;
|
||||
|
||||
// Always update the maximum scoring values.
|
||||
applyResult(result.Judgement.MaxResult, ref currentMaximumScoringValues);
|
||||
currentMaximumScoringValues.MaxCombo += result.Judgement.MaxResult.IncreasesCombo() ? 1 : 0;
|
||||
|
||||
if (!result.Type.IsScorable())
|
||||
return;
|
||||
|
||||
@ -171,16 +191,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
else if (result.Type.BreaksCombo())
|
||||
Combo.Value = 0;
|
||||
|
||||
double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0;
|
||||
|
||||
if (!result.Type.IsBonus())
|
||||
{
|
||||
baseScore += scoreIncrease;
|
||||
rollingMaxBaseScore += result.Judgement.MaxNumericResult;
|
||||
}
|
||||
|
||||
if (result.Type.IsBasic())
|
||||
basicHitObjects++;
|
||||
applyResult(result.Type, ref currentScoringValues);
|
||||
currentScoringValues.MaxCombo = HighestCombo.Value;
|
||||
|
||||
hitEvents.Add(CreateHitEvent(result));
|
||||
lastHitObject = result.HitObject;
|
||||
@ -188,6 +200,20 @@ namespace osu.Game.Rulesets.Scoring
|
||||
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>
|
||||
/// Creates the <see cref="HitEvent"/> that describes a <see cref="JudgementResult"/>.
|
||||
/// </summary>
|
||||
@ -206,19 +232,15 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
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())
|
||||
return;
|
||||
|
||||
double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0;
|
||||
|
||||
if (!result.Type.IsBonus())
|
||||
{
|
||||
baseScore -= scoreIncrease;
|
||||
rollingMaxBaseScore -= result.Judgement.MaxNumericResult;
|
||||
}
|
||||
|
||||
if (result.Type.IsBasic())
|
||||
basicHitObjects--;
|
||||
revertResult(result.Type, ref currentScoringValues);
|
||||
currentScoringValues.MaxCombo = HighestCombo.Value;
|
||||
|
||||
Debug.Assert(hitEvents.Count > 0);
|
||||
lastHitObject = hitEvents[^1].LastHitObject;
|
||||
@ -227,14 +249,24 @@ namespace osu.Game.Rulesets.Scoring
|
||||
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()
|
||||
{
|
||||
double rollingAccuracyRatio = rollingMaxBaseScore > 0 ? baseScore / rollingMaxBaseScore : 1;
|
||||
double accuracyRatio = maxBaseScore > 0 ? baseScore / maxBaseScore : 1;
|
||||
double comboRatio = maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1;
|
||||
|
||||
Accuracy.Value = rollingAccuracyRatio;
|
||||
TotalScore.Value = ComputeScore(Mode.Value, accuracyRatio, comboRatio, getBonusScore(scoreResultCounts), maxBasicHitObjects);
|
||||
Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1;
|
||||
TotalScore.Value = ComputeScore(Mode.Value, currentScoringValues, maximumScoringValues);
|
||||
}
|
||||
|
||||
/// <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="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
|
||||
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
|
||||
[Pure]
|
||||
public double ComputeFinalScore(ScoringMode mode, ScoreInfo scoreInfo)
|
||||
{
|
||||
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
||||
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
|
||||
|
||||
extractFromStatistics(ruleset,
|
||||
scoreInfo.Statistics,
|
||||
out double extractedBaseScore,
|
||||
out double extractedMaxBaseScore,
|
||||
out int extractedMaxCombo,
|
||||
out int extractedBasicHitObjects);
|
||||
ExtractScoringValues(scoreInfo, out var current, out var maximum);
|
||||
|
||||
double accuracyRatio = extractedMaxBaseScore > 0 ? extractedBaseScore / extractedMaxBaseScore : 1;
|
||||
double comboRatio = extractedMaxCombo > 0 ? (double)scoreInfo.MaxCombo / extractedMaxCombo : 1;
|
||||
|
||||
return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), extractedBasicHitObjects);
|
||||
return ComputeScore(mode, current, maximum);
|
||||
}
|
||||
|
||||
/// <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="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
|
||||
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
|
||||
[Pure]
|
||||
public double ComputePartialScore(ScoringMode mode, ScoreInfo scoreInfo)
|
||||
{
|
||||
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
||||
@ -281,17 +307,9 @@ namespace osu.Game.Rulesets.Scoring
|
||||
if (!beatmapApplied)
|
||||
throw new InvalidOperationException($"Cannot compute partial score without calling {nameof(ApplyBeatmap)}.");
|
||||
|
||||
extractFromStatistics(ruleset,
|
||||
scoreInfo.Statistics,
|
||||
out double extractedBaseScore,
|
||||
out _,
|
||||
out _,
|
||||
out _);
|
||||
ExtractScoringValues(scoreInfo, out var current, out _);
|
||||
|
||||
double accuracyRatio = maxBaseScore > 0 ? extractedBaseScore / maxBaseScore : 1;
|
||||
double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1;
|
||||
|
||||
return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), maxBasicHitObjects);
|
||||
return ComputeScore(mode, current, MaximumScoringValues);
|
||||
}
|
||||
|
||||
/// <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="maxAchievableCombo">The maximum achievable combo for the provided beatmap.</param>
|
||||
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
|
||||
[Pure]
|
||||
public double ComputeFinalLegacyScore(ScoringMode mode, ScoreInfo scoreInfo, int maxAchievableCombo)
|
||||
{
|
||||
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
||||
@ -313,26 +332,30 @@ namespace osu.Game.Rulesets.Scoring
|
||||
double accuracyRatio = scoreInfo.Accuracy;
|
||||
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.
|
||||
// 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.
|
||||
if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3)
|
||||
{
|
||||
extractFromStatistics(
|
||||
ruleset,
|
||||
scoreInfo.Statistics,
|
||||
out double computedBaseScore,
|
||||
out double computedMaxBaseScore,
|
||||
out _,
|
||||
out _);
|
||||
if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3 && maximum.BaseScore > 0)
|
||||
accuracyRatio = current.BaseScore / maximum.BaseScore;
|
||||
|
||||
if (computedMaxBaseScore > 0)
|
||||
accuracyRatio = computedBaseScore / computedMaxBaseScore;
|
||||
}
|
||||
return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.CountBasicHitObjects);
|
||||
}
|
||||
|
||||
int computedBasicHitObjects = scoreInfo.Statistics.Where(kvp => kvp.Key.IsBasic()).Select(kvp => kvp.Value).Sum();
|
||||
|
||||
return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), computedBasicHitObjects);
|
||||
/// <summary>
|
||||
/// Computes the total score from scoring values.
|
||||
/// </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>
|
||||
@ -344,6 +367,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// <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>
|
||||
/// <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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (acc == 1)
|
||||
@ -402,15 +417,10 @@ namespace osu.Game.Rulesets.Scoring
|
||||
lastHitObject = null;
|
||||
|
||||
if (storeResults)
|
||||
{
|
||||
maxAchievableCombo = HighestCombo.Value;
|
||||
maxBaseScore = baseScore;
|
||||
maxBasicHitObjects = basicHitObjects;
|
||||
}
|
||||
maximumScoringValues = currentScoringValues;
|
||||
|
||||
baseScore = 0;
|
||||
rollingMaxBaseScore = 0;
|
||||
basicHitObjects = 0;
|
||||
currentScoringValues = default;
|
||||
currentMaximumScoringValues = default;
|
||||
|
||||
TotalScore.Value = 0;
|
||||
Accuracy.Value = 1;
|
||||
@ -437,14 +447,19 @@ namespace osu.Game.Rulesets.Scoring
|
||||
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)
|
||||
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;
|
||||
|
||||
scoreResultCounts.Clear();
|
||||
@ -455,52 +470,126 @@ namespace osu.Game.Rulesets.Scoring
|
||||
OnResetFromReplayFrame?.Invoke();
|
||||
}
|
||||
|
||||
private void extractFromStatistics(Ruleset ruleset, IReadOnlyDictionary<HitResult, int> statistics, out double baseScore, out double maxBaseScore, out int maxCombo,
|
||||
out int basicHitObjects)
|
||||
#region ScoringValue extraction
|
||||
|
||||
/// <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;
|
||||
maxBaseScore = 0;
|
||||
maxCombo = 0;
|
||||
basicHitObjects = 0;
|
||||
extractScoringValues(scoreInfo.Statistics, out current, out maximum);
|
||||
current.MaxCombo = scoreInfo.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 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)
|
||||
{
|
||||
// Bonus scores are counted separately directly from the statistics dictionary later on.
|
||||
if (!result.IsScorable() || result.IsBonus())
|
||||
if (!result.IsScorable())
|
||||
continue;
|
||||
|
||||
// The maximum result of this judgement if it wasn't a miss.
|
||||
// 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).
|
||||
HitResult maxResult;
|
||||
|
||||
switch (result)
|
||||
if (result.IsBonus())
|
||||
current.BonusScore += count * Judgement.ToNumericResult(result);
|
||||
else
|
||||
{
|
||||
case HitResult.LargeTickHit:
|
||||
case HitResult.LargeTickMiss:
|
||||
maxResult = HitResult.LargeTickHit;
|
||||
break;
|
||||
// The maximum result of this judgement if it wasn't a miss.
|
||||
// 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).
|
||||
HitResult maxResult;
|
||||
|
||||
case HitResult.SmallTickHit:
|
||||
case HitResult.SmallTickMiss:
|
||||
maxResult = HitResult.SmallTickHit;
|
||||
break;
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.LargeTickHit:
|
||||
case HitResult.LargeTickMiss:
|
||||
maxResult = HitResult.LargeTickHit;
|
||||
break;
|
||||
|
||||
default:
|
||||
maxResult = maxBasicResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result;
|
||||
break;
|
||||
case HitResult.SmallTickHit:
|
||||
case HitResult.SmallTickMiss:
|
||||
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())
|
||||
maxCombo += count;
|
||||
maximum.MaxCombo += count;
|
||||
|
||||
if (result.IsBasic())
|
||||
basicHitObjects += count;
|
||||
{
|
||||
current.CountBasicHitObjects += count;
|
||||
maximum.CountBasicHitObjects += count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
@ -27,8 +27,6 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
public readonly KeyBindingContainer<T> KeyBindingContainer;
|
||||
|
||||
private readonly Ruleset ruleset;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private ScoreProcessor scoreProcessor { get; set; }
|
||||
|
||||
@ -57,8 +55,6 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
|
||||
{
|
||||
this.ruleset = ruleset.CreateInstance();
|
||||
|
||||
InternalChild = KeyBindingContainer =
|
||||
CreateKeyBindingContainer(ruleset, variant, unique)
|
||||
.WithChild(content = new Container { RelativeSizeAxes = Axes.Both });
|
||||
@ -85,7 +81,7 @@ namespace osu.Game.Rulesets.UI
|
||||
break;
|
||||
|
||||
case ReplayStatisticsFrameEvent statisticsStateChangeEvent:
|
||||
scoreProcessor?.ResetFromReplayFrame(ruleset, statisticsStateChangeEvent.Frame);
|
||||
scoreProcessor?.ResetFromReplayFrame(statisticsStateChangeEvent.Frame);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
41
osu.Game/Scoring/ScoringValues.cs
Normal file
41
osu.Game/Scoring/ScoringValues.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -1,99 +1,29 @@
|
||||
// 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.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.Framework.Allocation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Edit.Timing;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public class TimelineButton : CompositeDrawable
|
||||
public class TimelineButton : IconButton
|
||||
{
|
||||
public Action Action;
|
||||
public readonly BindableBool Enabled = new BindableBool(true);
|
||||
|
||||
public IconUsage Icon
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
get => button.Icon;
|
||||
set => button.Icon = value;
|
||||
// These are using colourProvider but don't match the design.
|
||||
// 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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Transforms;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.Containers;
|
||||
@ -40,10 +41,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
[Resolved(canBeNull: true)]
|
||||
private IFrameBasedClock editorClock { get; set; }
|
||||
|
||||
private readonly LayoutValue zoomedContentWidthCache = new LayoutValue(Invalidation.DrawSize);
|
||||
|
||||
public ZoomableScrollContainer()
|
||||
: base(Direction.Horizontal)
|
||||
{
|
||||
base.Content.Add(zoomedContent = new Container { RelativeSizeAxes = Axes.Y });
|
||||
|
||||
AddLayout(zoomedContentWidthCache);
|
||||
}
|
||||
|
||||
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.
|
||||
updateZoomedContentWidth();
|
||||
if (!zoomedContentWidthCache.IsValid)
|
||||
updateZoomedContentWidth();
|
||||
}
|
||||
|
||||
protected override bool OnScroll(ScrollEvent e)
|
||||
@ -128,7 +133,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
return base.OnScroll(e);
|
||||
}
|
||||
|
||||
private void updateZoomedContentWidth() => zoomedContent.Width = DrawWidth * currentZoom;
|
||||
private void updateZoomedContentWidth()
|
||||
{
|
||||
zoomedContent.Width = DrawWidth * currentZoom;
|
||||
zoomedContentWidthCache.Validate();
|
||||
}
|
||||
|
||||
private float zoomTarget = 1;
|
||||
|
||||
@ -199,8 +208,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
float targetOffset = expectedWidth * (focusPoint / contentSize) - focusOffset;
|
||||
|
||||
d.currentZoom = newZoom;
|
||||
|
||||
d.updateZoomedContentWidth();
|
||||
|
||||
// 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.
|
||||
d.Invalidate(Invalidation.DrawSize);
|
||||
|
@ -1,6 +1,8 @@
|
||||
// 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.IO;
|
||||
@ -9,6 +11,7 @@ using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
@ -27,10 +30,10 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
|
||||
public IEnumerable<string> HandledExtensions => handledExtensions;
|
||||
|
||||
private readonly Bindable<FileInfo> currentFile = new Bindable<FileInfo>();
|
||||
private readonly Bindable<FileInfo?> currentFile = new Bindable<FileInfo?>();
|
||||
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; }
|
||||
private OsuGameBase game { get; set; } = null!;
|
||||
|
||||
public FileChooserLabelledTextBox(params string[] handledExtensions)
|
||||
{
|
||||
@ -45,7 +48,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
currentFile.BindValueChanged(onFileSelected);
|
||||
}
|
||||
|
||||
private void onFileSelected(ValueChangedEvent<FileInfo> file)
|
||||
private void onFileSelected(ValueChangedEvent<FileInfo?> file)
|
||||
{
|
||||
if (file.NewValue == null)
|
||||
return;
|
||||
@ -65,14 +68,16 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
game.UnregisterImportHandler(this);
|
||||
|
||||
if (game.IsNotNull())
|
||||
game.UnregisterImportHandler(this);
|
||||
}
|
||||
|
||||
public override Popover GetPopover() => new FileChooserPopover(handledExtensions, currentFile);
|
||||
|
||||
private class FileChooserPopover : OsuPopover
|
||||
{
|
||||
public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo> currentFile)
|
||||
public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> currentFile)
|
||||
{
|
||||
Child = new Container
|
||||
{
|
||||
|
@ -37,7 +37,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
Padding = new MarginPadding(10);
|
||||
Padding = new MarginPadding(10) { Bottom = 0 };
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
|
@ -38,6 +38,8 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
[Resolved]
|
||||
private OverlayColourProvider overlayColourProvider { get; set; }
|
||||
|
||||
public bool EnableClicking { get; set; } = true;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
@ -281,6 +283,9 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
if (!EnableClicking)
|
||||
return;
|
||||
|
||||
var channel = clunk?.GetChannel();
|
||||
|
||||
if (channel != null)
|
||||
|
92
osu.Game/Screens/Edit/Timing/RepeatingButtonBehaviour.cs
Normal file
92
osu.Game/Screens/Edit/Timing/RepeatingButtonBehaviour.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -77,7 +77,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
Flow = new FillFlowContainer
|
||||
{
|
||||
Padding = new MarginPadding(20),
|
||||
Padding = new MarginPadding(10) { Top = 0 },
|
||||
Spacing = new Vector2(20),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
|
418
osu.Game/Screens/Edit/Timing/TapButton.cs
Normal file
418
osu.Game/Screens/Edit/Timing/TapButton.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,33 +1,45 @@
|
||||
// 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.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
public class TapTimingControl : CompositeDrawable
|
||||
{
|
||||
[Resolved]
|
||||
private EditorClock editorClock { get; set; }
|
||||
private EditorClock editorClock { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap beatmap { get; set; }
|
||||
private EditorBeatmap beatmap { get; set; } = null!;
|
||||
|
||||
[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]
|
||||
private void load(OverlayColourProvider colourProvider, OsuColour colours)
|
||||
{
|
||||
const float padding = 10;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
@ -48,8 +60,8 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.Absolute, 200),
|
||||
new Dimension(GridSizeMode.Absolute, 60),
|
||||
new Dimension(GridSizeMode.Absolute, 60),
|
||||
new Dimension(GridSizeMode.Absolute, 50),
|
||||
new Dimension(GridSizeMode.Absolute, TapButton.SIZE + padding),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
@ -58,6 +70,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(padding),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new GridContainer
|
||||
@ -72,7 +85,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new MetronomeDisplay
|
||||
metronome = new MetronomeDisplay
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
@ -89,15 +102,14 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(10),
|
||||
Padding = new MarginPadding { Bottom = padding, Horizontal = padding },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new TimingAdjustButton(1)
|
||||
{
|
||||
Text = "Offset",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.48f,
|
||||
Height = 50,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0.48f, 1),
|
||||
Action = adjustOffset,
|
||||
},
|
||||
new TimingAdjustButton(0.1)
|
||||
@ -105,9 +117,8 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Text = "BPM",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.48f,
|
||||
Height = 50,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0.48f, 1),
|
||||
Action = adjustBpm,
|
||||
}
|
||||
}
|
||||
@ -118,33 +129,70 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(10),
|
||||
Padding = new MarginPadding { Bottom = padding, Horizontal = padding },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new RoundedButton
|
||||
new Container
|
||||
{
|
||||
Text = "Reset",
|
||||
BackgroundColour = colours.Pink,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0.3f,
|
||||
Action = reset,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.CentreRight,
|
||||
Height = 0.98f,
|
||||
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,
|
||||
Origin = Anchor.TopRight,
|
||||
Text = "Play from start",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
BackgroundColour = colourProvider.Background1,
|
||||
Width = 0.68f,
|
||||
Action = tap,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
IsHandlingTapping = { BindTarget = isHandlingTapping }
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
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)
|
||||
@ -176,16 +224,66 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
timing.BeatLength = 60000 / (timing.BPM + adjust);
|
||||
}
|
||||
|
||||
private void tap()
|
||||
private class InlineButton : OsuButton
|
||||
{
|
||||
editorClock.Seek(selectedGroup.Value.Time);
|
||||
editorClock.Start();
|
||||
}
|
||||
private readonly IconUsage icon;
|
||||
private readonly Anchor anchor;
|
||||
|
||||
private void reset()
|
||||
{
|
||||
editorClock.Stop();
|
||||
editorClock.Seek(selectedGroup.Value.Time);
|
||||
private SpriteIcon spriteIcon = null!;
|
||||
|
||||
[Resolved]
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,14 +4,11 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
@ -26,32 +23,24 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
public Action<double> Action;
|
||||
|
||||
private readonly double adjustAmount;
|
||||
private ScheduledDelegate adjustDelegate;
|
||||
|
||||
private const int max_multiplier = 10;
|
||||
|
||||
private const int adjust_levels = 4;
|
||||
|
||||
private const double initial_delay = 300;
|
||||
|
||||
private const double minimum_delay = 80;
|
||||
|
||||
public Container Content { get; set; }
|
||||
|
||||
private double adjustDelay = initial_delay;
|
||||
|
||||
private readonly Box background;
|
||||
|
||||
private readonly OsuSpriteText text;
|
||||
|
||||
private Sample sample;
|
||||
|
||||
public LocalisableString Text
|
||||
{
|
||||
get => text.Text;
|
||||
set => text.Text = value;
|
||||
}
|
||||
|
||||
private readonly RepeatingButtonBehaviour repeatBehaviour;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; }
|
||||
|
||||
@ -82,13 +71,13 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddInternal(repeatBehaviour = new RepeatingButtonBehaviour(this));
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
private void load()
|
||||
{
|
||||
sample = audio.Samples.Get(@"UI/notch-tick");
|
||||
|
||||
background.Colour = colourProvider.Background3;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
public readonly float Multiplier;
|
||||
@ -187,7 +141,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
Origin = direction,
|
||||
Font = OsuFont.Default.With(size: 10, weight: FontWeight.Bold),
|
||||
Text = $"{(index > 0 ? "+" : "-")}{Math.Abs(Multiplier * amount)}",
|
||||
Padding = new MarginPadding(5),
|
||||
Padding = new MarginPadding(2),
|
||||
Alpha = 0,
|
||||
}
|
||||
};
|
||||
|
@ -1,12 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -51,6 +53,8 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
|
||||
|
||||
private RoundedButton addButton;
|
||||
|
||||
[Resolved]
|
||||
private EditorClock clock { get; set; }
|
||||
|
||||
@ -105,9 +109,8 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
},
|
||||
new RoundedButton
|
||||
addButton = new RoundedButton
|
||||
{
|
||||
Text = "+ Add at current time",
|
||||
Action = addNew,
|
||||
Size = new Vector2(160, 30),
|
||||
Anchor = Anchor.BottomRight,
|
||||
@ -122,7 +125,14 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
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.BindCollectionChanged((sender, args) =>
|
||||
@ -132,13 +142,23 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
selectedGroup.Value = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
trackActivePoint();
|
||||
|
||||
addButton.Enabled.Value = clock.CurrentTimeAccurate != selectedGroup.Value?.Time;
|
||||
}
|
||||
|
||||
private Type trackedType;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
@ -149,16 +169,27 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
private void trackActivePoint()
|
||||
{
|
||||
// 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.
|
||||
// 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.
|
||||
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);
|
||||
|
||||
if (found != null)
|
||||
@ -189,7 +220,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
// Try and create matching types from the currently selected control point.
|
||||
var selected = selectedGroup.Value;
|
||||
|
||||
if (selected != null)
|
||||
if (selected != null && selected != group)
|
||||
{
|
||||
foreach (var controlPoint in selected.ControlPoints)
|
||||
{
|
||||
|
@ -55,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
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[]
|
||||
{
|
||||
|
@ -8,6 +8,7 @@ using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -244,7 +245,7 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ namespace osu.Game.Screens.Select.Filter
|
||||
[Description("Author")]
|
||||
Author,
|
||||
|
||||
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatsBpm))]
|
||||
[LocalisableDescription(typeof(SortStrings), nameof(SortStrings.ArtistTracksBpm))]
|
||||
BPM,
|
||||
|
||||
[Description("Date Added")]
|
||||
@ -28,10 +28,10 @@ namespace osu.Game.Screens.Select.Filter
|
||||
Length,
|
||||
|
||||
// todo: pending support (https://github.com/ppy/osu/issues/4917)
|
||||
// [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchFiltersRank))]
|
||||
// [Description("Rank Achieved")]
|
||||
// RankAchieved,
|
||||
|
||||
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoSource))]
|
||||
[Description("Source")]
|
||||
Source,
|
||||
|
||||
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchSortingTitle))]
|
||||
|
@ -66,7 +66,7 @@ namespace osu.Game.Screens.Select
|
||||
private readonly Box light;
|
||||
|
||||
public FooterButton()
|
||||
: base(HoverSampleSet.Button)
|
||||
: base(HoverSampleSet.Toolbar)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Shear = SHEAR;
|
||||
|
@ -77,7 +77,7 @@ namespace osu.Game.Screens.Select.Options
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
public BeatmapOptionsButton()
|
||||
: base(HoverSampleSet.Submit)
|
||||
: base(HoverSampleSet.Button)
|
||||
{
|
||||
Width = width;
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Skinning
|
||||
/// The <see cref="ISkin"/> which is being transformed.
|
||||
/// </summary>
|
||||
[NotNull]
|
||||
protected internal ISkin Skin { get; }
|
||||
public ISkin Skin { get; }
|
||||
|
||||
protected LegacySkinTransformer([NotNull] ISkin skin)
|
||||
{
|
||||
|
@ -3,7 +3,9 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
@ -44,9 +46,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay
|
||||
switch (request)
|
||||
{
|
||||
case CreateRoomRequest createRoomRequest:
|
||||
var apiRoom = new Room();
|
||||
|
||||
apiRoom.CopyFrom(createRoomRequest.Room);
|
||||
var apiRoom = cloneRoom(createRoomRequest.Room);
|
||||
|
||||
// Passwords are explicitly not copied between rooms.
|
||||
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)
|
||||
{
|
||||
var responseRoom = new Room();
|
||||
responseRoom.CopyFrom(room);
|
||||
var responseRoom = cloneRoom(room);
|
||||
|
||||
// Password is hidden from the response, and is only propagated via HasPassword.
|
||||
bool hadPassword = responseRoom.HasPassword.Value;
|
||||
responseRoom.Password.Value = null;
|
||||
responseRoom.HasPassword.Value = hadPassword;
|
||||
|
||||
if (!withParticipants)
|
||||
responseRoom.RecentParticipants.Clear();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
|
||||
namespace osu.Game.Users.Drawables
|
||||
@ -74,11 +73,6 @@ namespace osu.Game.Users.Drawables
|
||||
{
|
||||
private LocalisableString tooltip = default_tooltip_text;
|
||||
|
||||
public ClickableArea()
|
||||
: base(HoverSampleSet.Submit)
|
||||
{
|
||||
}
|
||||
|
||||
public override LocalisableString TooltipText
|
||||
{
|
||||
get => Enabled.Value ? tooltip : default;
|
||||
|
@ -49,7 +49,7 @@ namespace osu.Game.Users.Drawables
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
new HoverClickSounds(HoverSampleSet.Submit)
|
||||
new HoverClickSounds()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Users
|
||||
protected Drawable Background { get; private set; }
|
||||
|
||||
protected UserPanel(APIUser user)
|
||||
: base(HoverSampleSet.Submit)
|
||||
: base(HoverSampleSet.Button)
|
||||
{
|
||||
if (user == null)
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
|
@ -43,7 +43,7 @@ namespace osu.Game.Utils
|
||||
sentrySession = SentrySdk.Init(options =>
|
||||
{
|
||||
// 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.AutoSessionTracking = true;
|
||||
@ -159,6 +159,7 @@ namespace osu.Game.Utils
|
||||
Game = game.Clock.CurrentTime,
|
||||
};
|
||||
|
||||
scope.SetTag(@"beatmap", $"{beatmap.OnlineID}");
|
||||
scope.SetTag(@"ruleset", ruleset.ShortName);
|
||||
scope.SetTag(@"os", $"{RuntimeInfo.OS} ({Environment.OSVersion})");
|
||||
scope.SetTag(@"processor count", Environment.ProcessorCount.ToString());
|
||||
|
@ -35,8 +35,8 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="10.11.2" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2022.530.0" />
|
||||
<PackageReference Include="Realm" Version="10.14.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2022.605.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.527.0" />
|
||||
<PackageReference Include="Sentry" Version="3.17.1" />
|
||||
<PackageReference Include="SharpCompress" Version="0.31.0" />
|
||||
|
@ -61,7 +61,7 @@
|
||||
<Reference Include="System.Net.Http" />
|
||||
</ItemGroup>
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
<!-- 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.Core" Version="5.0.14" />
|
||||
<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="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
<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>
|
||||
</Project>
|
||||
|
Loading…
Reference in New Issue
Block a user