1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 07:27:25 +08:00

Merge branch 'master' into multiplayer-leaderboard-user-mods-2

This commit is contained in:
Bartłomiej Dach 2022-06-02 20:45:10 +02:00
commit 59ffc8b08e
No known key found for this signature in database
GPG Key ID: BCECCD4FA41F6497
32 changed files with 867 additions and 125 deletions

View File

@ -23,4 +23,4 @@ jobs:
SENTRY_URL: https://sentry.ppy.sh/ SENTRY_URL: https://sentry.ppy.sh/
with: with:
environment: production environment: production
version: ${{ github.ref }} version: osu@${{ github.ref_name }}

View File

@ -77,6 +77,12 @@ namespace osu.Game.Tests.Visual.Editing
timingInfo.Text = $"offset: {selectedGroup.Value.Time:N2} bpm: {selectedGroup.Value.ControlPoints.OfType<TimingControlPoint>().First().BPM:N2}"; timingInfo.Text = $"offset: {selectedGroup.Value.Time:N2} bpm: {selectedGroup.Value.ControlPoints.OfType<TimingControlPoint>().First().BPM:N2}";
} }
[Test]
public void TestNoop()
{
AddStep("do nothing", () => { });
}
[Test] [Test]
public void TestTapThenReset() public void TestTapThenReset()
{ {

View File

@ -1,14 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Timing;
using osu.Game.Screens.Edit.Timing.RowAttributes;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
{ {
@ -22,6 +26,8 @@ namespace osu.Game.Tests.Visual.Editing
[Cached] [Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
private TimingScreen timingScreen;
protected override bool ScrollUsingMouseWheel => false; protected override bool ScrollUsingMouseWheel => false;
public TestSceneTimingScreen() public TestSceneTimingScreen()
@ -36,12 +42,54 @@ namespace osu.Game.Tests.Visual.Editing
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap); Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
Beatmap.Disabled = true; Beatmap.Disabled = true;
Child = new TimingScreen Child = timingScreen = new TimingScreen
{ {
State = { Value = Visibility.Visible }, State = { Value = Visibility.Visible },
}; };
} }
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Stop clock", () => Clock.Stop());
AddUntilStep("wait for rows to load", () => Child.ChildrenOfType<EffectRowAttribute>().Any());
}
[Test]
public void TestTrackingCurrentTimeWhileRunning()
{
AddStep("Select first effect point", () =>
{
InputManager.MoveMouseTo(Child.ChildrenOfType<EffectRowAttribute>().First());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670);
AddUntilStep("Ensure seeked to correct time", () => Clock.CurrentTimeAccurate == 54670);
AddStep("Seek to just before next point", () => Clock.Seek(69000));
AddStep("Start clock", () => Clock.Start());
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670);
}
[Test]
public void TestTrackingCurrentTimeWhilePaused()
{
AddStep("Select first effect point", () =>
{
InputManager.MoveMouseTo(Child.ChildrenOfType<EffectRowAttribute>().First());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670);
AddUntilStep("Ensure seeked to correct time", () => Clock.CurrentTimeAccurate == 54670);
AddStep("Seek to later", () => Clock.Seek(80000));
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670);
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
Beatmap.Disabled = false; Beatmap.Disabled = false;

View File

@ -469,6 +469,8 @@ namespace osu.Game.Tests.Visual.Online
chatOverlay.Show(); chatOverlay.Show();
}); });
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
waitForChannel1Visible(); waitForChannel1Visible();
AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext)); AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext));
waitForChannel2Visible(); waitForChannel2Visible();

View File

@ -173,6 +173,8 @@ namespace osu.Game.Tests.Visual.Playlists
{ {
AddUntilStep("wait for scores loaded", () => AddUntilStep("wait for scores loaded", () =>
requestComplete requestComplete
// request handler may need to fire more than once to get scores.
&& totalCount > 0
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount && resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount
&& resultsScreen.ScorePanelList.AllPanelsVisible); && resultsScreen.ScorePanelList.AllPanelsVisible);
AddWaitStep("wait for display", 5); AddWaitStep("wait for display", 5);

View File

@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
@ -83,7 +84,7 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("clear label", () => textBox.LabelText = default); AddStep("clear label", () => textBox.LabelText = default);
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1)); AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
AddStep("set warning text", () => textBox.WarningText = "This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator..."); AddStep("set warning text", () => textBox.SetNoticeText("This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator...", true));
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1)); AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
} }
@ -129,16 +130,18 @@ namespace osu.Game.Tests.Visual.Settings
SettingsNumberBox numberBox = null; SettingsNumberBox numberBox = null;
AddStep("create settings item", () => Child = numberBox = new SettingsNumberBox()); AddStep("create settings item", () => Child = numberBox = new SettingsNumberBox());
AddAssert("warning text not created", () => !numberBox.ChildrenOfType<SettingsNoticeText>().Any()); AddAssert("warning text not created", () => !numberBox.ChildrenOfType<LinkFlowContainer>().Any());
AddStep("set warning text", () => numberBox.WarningText = "this is a warning!"); AddStep("set warning text", () => numberBox.SetNoticeText("this is a warning!", true));
AddAssert("warning text created", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 1); AddAssert("warning text created", () => numberBox.ChildrenOfType<LinkFlowContainer>().Single().Alpha == 1);
AddStep("unset warning text", () => numberBox.WarningText = default); AddStep("unset warning text", () => numberBox.ClearNoticeText());
AddAssert("warning text hidden", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 0); AddAssert("warning text hidden", () => !numberBox.ChildrenOfType<LinkFlowContainer>().Any());
AddStep("set warning text again", () => numberBox.WarningText = "another warning!"); AddStep("set warning text again", () => numberBox.SetNoticeText("another warning!", true));
AddAssert("warning text shown again", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 1); AddAssert("warning text shown again", () => numberBox.ChildrenOfType<LinkFlowContainer>().Single().Alpha == 1);
AddStep("set non warning text", () => numberBox.SetNoticeText("you did good!"));
} }
} }
} }

View File

@ -85,6 +85,8 @@ namespace osu.Game.Beatmaps.Drawables
downloadTrackers.Add(beatmapDownloadTracker); downloadTrackers.Add(beatmapDownloadTracker);
AddInternal(beatmapDownloadTracker); AddInternal(beatmapDownloadTracker);
// Note that this is downloading the beatmaps even if they are already downloaded.
// We could rely more on `BeatmapDownloadTracker`'s exposed state to avoid this.
beatmapDownloader.Download(beatmapSet); beatmapDownloader.Download(beatmapSet);
} }
} }

View File

@ -91,7 +91,7 @@ namespace osu.Game.Database
} }
} }
private void performLookup() private async Task performLookup()
{ {
// contains at most 50 unique IDs from tasks, which is used to perform the lookup. // contains at most 50 unique IDs from tasks, which is used to perform the lookup.
var nextTaskBatch = new Dictionary<TLookup, List<TaskCompletionSource<TValue>>>(); var nextTaskBatch = new Dictionary<TLookup, List<TaskCompletionSource<TValue>>>();
@ -127,7 +127,7 @@ namespace osu.Game.Database
// rather than queueing, we maintain our own single-threaded request stream. // rather than queueing, we maintain our own single-threaded request stream.
// todo: we probably want retry logic here. // todo: we probably want retry logic here.
api.Perform(request); await api.PerformAsync(request).ConfigureAwait(false);
finishPendingTask(); finishPendingTask();

View File

@ -0,0 +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 osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class LayoutSettingsStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.LayoutSettings";
/// <summary>
/// "Checking for fullscreen capabilities..."
/// </summary>
public static LocalisableString CheckingForFullscreenCapabilities => new TranslatableString(getKey(@"checking_for_fullscreen_capabilities"), @"Checking for fullscreen capabilities...");
/// <summary>
/// "osu! is running exclusive fullscreen, guaranteeing low latency!"
/// </summary>
public static LocalisableString OsuIsRunningExclusiveFullscreen => new TranslatableString(getKey(@"osu_is_running_exclusive_fullscreen"), @"osu! is running exclusive fullscreen, guaranteeing low latency!");
/// <summary>
/// "Unable to run exclusive fullscreen. You&#39;ll still experience some input latency."
/// </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.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -63,12 +63,13 @@ namespace osu.Game.Online.API
public virtual void Queue(APIRequest request) public virtual void Queue(APIRequest request)
{ {
if (HandleRequest?.Invoke(request) != true) Schedule(() =>
{ {
// this will fail due to not receiving an APIAccess, and trigger a failure on the request. if (HandleRequest?.Invoke(request) != true)
// this is intended - any request in testing that needs non-failures should use HandleRequest. {
request.Perform(this); request.Fail(new InvalidOperationException($@"{nameof(DummyAPIAccess)} cannot process this request."));
} }
});
} }
public void Perform(APIRequest request) => HandleRequest?.Invoke(request); public void Perform(APIRequest request) => HandleRequest?.Invoke(request);

View File

@ -154,12 +154,15 @@ namespace osu.Game.Overlays.FirstRunSetup
var downloadTracker = tutorialDownloader.DownloadTrackers.First(); var downloadTracker = tutorialDownloader.DownloadTrackers.First();
downloadTracker.State.BindValueChanged(state =>
{
if (state.NewValue == DownloadState.LocallyAvailable)
downloadTutorialButton.Complete();
}, true);
downloadTracker.Progress.BindValueChanged(progress => downloadTracker.Progress.BindValueChanged(progress =>
{ {
downloadTutorialButton.SetProgress(progress.NewValue, false); downloadTutorialButton.SetProgress(progress.NewValue, false);
if (progress.NewValue == 1)
downloadTutorialButton.Complete();
}, true); }, true);
} }

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Platform.Windows;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -34,10 +35,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private Bindable<Size> sizeFullscreen; private Bindable<Size> sizeFullscreen;
private readonly BindableList<Size> resolutions = new BindableList<Size>(new[] { new Size(9999, 9999) }); private readonly BindableList<Size> resolutions = new BindableList<Size>(new[] { new Size(9999, 9999) });
private readonly IBindable<FullscreenCapability> fullscreenCapability = new Bindable<FullscreenCapability>(FullscreenCapability.Capable);
[Resolved] [Resolved]
private OsuGameBase game { get; set; } private OsuGameBase game { get; set; }
[Resolved]
private GameHost host { get; set; }
private SettingsDropdown<Size> resolutionDropdown; private SettingsDropdown<Size> resolutionDropdown;
private SettingsDropdown<Display> displayDropdown; private SettingsDropdown<Display> displayDropdown;
private SettingsDropdown<WindowMode> windowModeDropdown; private SettingsDropdown<WindowMode> windowModeDropdown;
@ -65,6 +70,9 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
windowModes.BindTo(host.Window.SupportedWindowModes); windowModes.BindTo(host.Window.SupportedWindowModes);
} }
if (host.Window is WindowsWindow windowsWindow)
fullscreenCapability.BindTo(windowsWindow.FullscreenCapability);
Children = new Drawable[] Children = new Drawable[]
{ {
windowModeDropdown = new SettingsDropdown<WindowMode> windowModeDropdown = new SettingsDropdown<WindowMode>
@ -139,6 +147,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
} }
}, },
}; };
fullscreenCapability.BindValueChanged(_ => Schedule(updateScreenModeWarning), true);
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -150,8 +160,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
windowModeDropdown.Current.BindValueChanged(mode => windowModeDropdown.Current.BindValueChanged(mode =>
{ {
updateDisplayModeDropdowns(); updateDisplayModeDropdowns();
updateScreenModeWarning();
windowModeDropdown.WarningText = mode.NewValue != WindowMode.Fullscreen ? GraphicsSettingsStrings.NotFullscreenNote : default;
}, true); }, true);
windowModes.BindCollectionChanged((sender, args) => windowModes.BindCollectionChanged((sender, args) =>
@ -213,6 +222,38 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
} }
} }
private void updateScreenModeWarning()
{
if (windowModeDropdown.Current.Value != WindowMode.Fullscreen)
{
windowModeDropdown.SetNoticeText(GraphicsSettingsStrings.NotFullscreenNote, true);
return;
}
if (host.Window is WindowsWindow)
{
switch (fullscreenCapability.Value)
{
case FullscreenCapability.Unknown:
windowModeDropdown.SetNoticeText(LayoutSettingsStrings.CheckingForFullscreenCapabilities, true);
break;
case FullscreenCapability.Capable:
windowModeDropdown.SetNoticeText(LayoutSettingsStrings.OsuIsRunningExclusiveFullscreen);
break;
case FullscreenCapability.Incapable:
windowModeDropdown.SetNoticeText(LayoutSettingsStrings.UnableToRunExclusiveFullscreen, true);
break;
}
}
else
{
// We can only detect exclusive fullscreen status on windows currently.
windowModeDropdown.ClearNoticeText();
}
}
private void bindPreviewEvent(Bindable<float> bindable) private void bindPreviewEvent(Bindable<float> bindable)
{ {
bindable.ValueChanged += _ => bindable.ValueChanged += _ =>

View File

@ -48,7 +48,16 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
frameLimiterDropdown.Current.BindValueChanged(limit => frameLimiterDropdown.Current.BindValueChanged(limit =>
{ {
frameLimiterDropdown.WarningText = limit.NewValue == FrameSync.Unlimited ? GraphicsSettingsStrings.UnlimitedFramesNote : default; switch (limit.NewValue)
{
case FrameSync.Unlimited:
frameLimiterDropdown.SetNoticeText(GraphicsSettingsStrings.UnlimitedFramesNote, true);
break;
default:
frameLimiterDropdown.ClearNoticeText();
break;
}
}, true); }, true);
} }
} }

View File

@ -117,9 +117,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
{ {
if (highPrecision.NewValue) if (highPrecision.NewValue)
highPrecisionMouse.WarningText = MouseSettingsStrings.HighPrecisionPlatformWarning; highPrecisionMouse.SetNoticeText(MouseSettingsStrings.HighPrecisionPlatformWarning, true);
else else
highPrecisionMouse.WarningText = null; highPrecisionMouse.ClearNoticeText();
} }
}, true); }, true);
} }

View File

@ -11,6 +11,7 @@ using osu.Framework.Localisation;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK; using osuTK;
using osu.Game.Localisation; using osu.Game.Localisation;
@ -95,11 +96,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Text = TabletSettingsStrings.NoTabletDetected, Text = TabletSettingsStrings.NoTabletDetected,
}, },
new SettingsNoticeText(colours) new LinkFlowContainer(cp => cp.Colour = colours.Yellow)
{ {
TextAnchor = Anchor.TopCentre, TextAnchor = Anchor.TopCentre,
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}.With(t => }.With(t =>
{ {
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows || RuntimeInfo.OS == RuntimeInfo.Platform.Linux) if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows || RuntimeInfo.OS == RuntimeInfo.Platform.Linux)

View File

@ -61,7 +61,10 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
user.BindValueChanged(u => user.BindValueChanged(u =>
{ {
backgroundSourceDropdown.WarningText = u.NewValue?.IsSupporter != true ? UserInterfaceStrings.NotSupporterNote : default; if (u.NewValue?.IsSupporter != true)
backgroundSourceDropdown.SetNoticeText(UserInterfaceStrings.NotSupporterNote, true);
else
backgroundSourceDropdown.ClearNoticeText();
}, true); }, true);
} }
} }

View File

@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Settings
private SpriteText labelText; private SpriteText labelText;
private OsuTextFlowContainer warningText; private OsuTextFlowContainer noticeText;
public bool ShowsDefaultIndicator = true; public bool ShowsDefaultIndicator = true;
private readonly Container defaultValueIndicatorContainer; private readonly Container defaultValueIndicatorContainer;
@ -70,27 +70,32 @@ namespace osu.Game.Overlays.Settings
} }
/// <summary> /// <summary>
/// Text to be displayed at the bottom of this <see cref="SettingsItem{T}"/>. /// Clear any warning text.
/// Generally used to recommend the user change their setting as the current one is considered sub-optimal.
/// </summary> /// </summary>
public LocalisableString? WarningText public void ClearNoticeText()
{ {
set noticeText?.Expire();
noticeText = null;
}
/// <summary>
/// Set the text to be displayed at the bottom of this <see cref="SettingsItem{T}"/>.
/// Generally used to provide feedback to a user about a sub-optimal setting.
/// </summary>
/// <param name="text">The text to display.</param>
/// <param name="isWarning">Whether the text is in a warning state. Will decide how this is visually represented.</param>
public void SetNoticeText(LocalisableString text, bool isWarning = false)
{
ClearNoticeText();
// construct lazily for cases where the label is not needed (may be provided by the Control).
FlowContent.Add(noticeText = new LinkFlowContainer(cp => cp.Colour = isWarning ? colours.Yellow : colours.Green)
{ {
bool hasValue = value != default; RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
if (warningText == null) Margin = new MarginPadding { Bottom = 5 },
{ Text = text,
if (!hasValue) });
return;
// construct lazily for cases where the label is not needed (may be provided by the Control).
FlowContent.Add(warningText = new SettingsNoticeText(colours) { Margin = new MarginPadding { Bottom = 5 } });
}
warningText.Alpha = hasValue ? 1 : 0;
warningText.Text = value ?? default;
}
} }
public virtual Bindable<T> Current public virtual Bindable<T> Current

View File

@ -1,19 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.Settings
{
public class SettingsNoticeText : LinkFlowContainer
{
public SettingsNoticeText(OsuColour colours)
: base(s => s.Colour = colours.Yellow)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
}
}
}

View File

@ -93,7 +93,18 @@ namespace osu.Game.Rulesets.Scoring
/// <summary> /// <summary>
/// Scoring values for a perfect play. /// Scoring values for a perfect play.
/// </summary> /// </summary>
public ScoringValues MaximumScoringValues { get; private set; } public ScoringValues MaximumScoringValues
{
get
{
if (!beatmapApplied)
throw new InvalidOperationException($"Cannot access maximum scoring values before calling {nameof(ApplyBeatmap)}.");
return maximumScoringValues;
}
}
private ScoringValues maximumScoringValues;
/// <summary> /// <summary>
/// Scoring values for the current play assuming all perfect hits. /// Scoring values for the current play assuming all perfect hits.
@ -200,7 +211,7 @@ namespace osu.Game.Rulesets.Scoring
scoringValues.BaseScore += result.IsHit() ? Judgement.ToNumericResult(result) : 0; scoringValues.BaseScore += result.IsHit() ? Judgement.ToNumericResult(result) : 0;
if (result.IsBasic()) if (result.IsBasic())
scoringValues.HitObjects++; scoringValues.CountBasicHitObjects++;
} }
/// <summary> /// <summary>
@ -249,13 +260,13 @@ namespace osu.Game.Rulesets.Scoring
scoringValues.BaseScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0; scoringValues.BaseScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0;
if (result.IsBasic()) if (result.IsBasic())
scoringValues.HitObjects--; scoringValues.CountBasicHitObjects--;
} }
private void updateScore() private void updateScore()
{ {
Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1; Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1;
TotalScore.Value = ComputeScore(Mode.Value, currentScoringValues, MaximumScoringValues); TotalScore.Value = ComputeScore(Mode.Value, currentScoringValues, maximumScoringValues);
} }
/// <summary> /// <summary>
@ -273,8 +284,7 @@ namespace osu.Game.Rulesets.Scoring
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
extractScoringValues(scoreInfo.Statistics, out var current, out var maximum); ExtractScoringValues(scoreInfo, out var current, out var maximum);
current.MaxCombo = scoreInfo.MaxCombo;
return ComputeScore(mode, current, maximum); return ComputeScore(mode, current, maximum);
} }
@ -297,8 +307,7 @@ namespace osu.Game.Rulesets.Scoring
if (!beatmapApplied) if (!beatmapApplied)
throw new InvalidOperationException($"Cannot compute partial score without calling {nameof(ApplyBeatmap)}."); throw new InvalidOperationException($"Cannot compute partial score without calling {nameof(ApplyBeatmap)}.");
extractScoringValues(scoreInfo.Statistics, out var current, out _); ExtractScoringValues(scoreInfo, out var current, out _);
current.MaxCombo = scoreInfo.MaxCombo;
return ComputeScore(mode, current, MaximumScoringValues); return ComputeScore(mode, current, MaximumScoringValues);
} }
@ -323,7 +332,7 @@ namespace osu.Game.Rulesets.Scoring
double accuracyRatio = scoreInfo.Accuracy; double accuracyRatio = scoreInfo.Accuracy;
double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1; double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1;
extractScoringValues(scoreInfo.Statistics, out var current, out var maximum); ExtractScoringValues(scoreInfo, out var current, out var maximum);
// For legacy osu!mania scores, a full-GREAT score has 100% accuracy. If combined with a full-combo, the score becomes indistinguishable from a full-PERFECT score. // For legacy osu!mania scores, a full-GREAT score has 100% accuracy. If combined with a full-combo, the score becomes indistinguishable from a full-PERFECT score.
// To get around this, the accuracy ratio is always recalculated based on the hit statistics rather than trusting the score. // To get around this, the accuracy ratio is always recalculated based on the hit statistics rather than trusting the score.
@ -331,7 +340,7 @@ namespace osu.Game.Rulesets.Scoring
if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3 && maximum.BaseScore > 0) if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3 && maximum.BaseScore > 0)
accuracyRatio = current.BaseScore / maximum.BaseScore; accuracyRatio = current.BaseScore / maximum.BaseScore;
return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.HitObjects); return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.CountBasicHitObjects);
} }
/// <summary> /// <summary>
@ -346,7 +355,7 @@ namespace osu.Game.Rulesets.Scoring
{ {
double accuracyRatio = maximum.BaseScore > 0 ? current.BaseScore / maximum.BaseScore : 1; double accuracyRatio = maximum.BaseScore > 0 ? current.BaseScore / maximum.BaseScore : 1;
double comboRatio = maximum.MaxCombo > 0 ? (double)current.MaxCombo / maximum.MaxCombo : 1; double comboRatio = maximum.MaxCombo > 0 ? (double)current.MaxCombo / maximum.MaxCombo : 1;
return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.HitObjects); return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.CountBasicHitObjects);
} }
/// <summary> /// <summary>
@ -408,7 +417,7 @@ namespace osu.Game.Rulesets.Scoring
lastHitObject = null; lastHitObject = null;
if (storeResults) if (storeResults)
MaximumScoringValues = currentScoringValues; maximumScoringValues = currentScoringValues;
currentScoringValues = default; currentScoringValues = default;
currentMaximumScoringValues = default; currentMaximumScoringValues = default;
@ -470,11 +479,11 @@ namespace osu.Game.Rulesets.Scoring
/// This method is useful in a variety of situations, with a few drawbacks that need to be considered: /// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
/// <list type="bullet"> /// <list type="bullet">
/// <item>The maximum <see cref="ScoringValues.BonusScore"/> 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.HitObjects"/> will always be the same value.</item> /// <item>The current and maximum <see cref="ScoringValues.CountBasicHitObjects"/> will always be the same value.</item>
/// </list> /// </list>
/// Consumers are expected to more accurately fill in the above values through external means. /// Consumers are expected to more accurately fill in the above values through external means.
/// <para> /// <para>
/// <b>Ensure</b> to fill in the maximum <see cref="ScoringValues.HitObjects"/> for use in /// <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)"/>. /// <see cref="ComputeScore(osu.Game.Rulesets.Scoring.ScoringMode,osu.Game.Scoring.ScoringValues,osu.Game.Scoring.ScoringValues)"/>.
/// </para> /// </para>
/// </remarks> /// </remarks>
@ -495,11 +504,11 @@ namespace osu.Game.Rulesets.Scoring
/// This method is useful in a variety of situations, with a few drawbacks that need to be considered: /// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
/// <list type="bullet"> /// <list type="bullet">
/// <item>The maximum <see cref="ScoringValues.BonusScore"/> 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.HitObjects"/> will always be the same value.</item> /// <item>The current and maximum <see cref="ScoringValues.CountBasicHitObjects"/> will always be the same value.</item>
/// </list> /// </list>
/// Consumers are expected to more accurately fill in the above values through external means. /// Consumers are expected to more accurately fill in the above values through external means.
/// <para> /// <para>
/// <b>Ensure</b> to fill in the maximum <see cref="ScoringValues.HitObjects"/> for use in /// <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)"/>. /// <see cref="ComputeScore(osu.Game.Rulesets.Scoring.ScoringMode,osu.Game.Scoring.ScoringValues,osu.Game.Scoring.ScoringValues)"/>.
/// </para> /// </para>
/// </remarks> /// </remarks>
@ -521,7 +530,7 @@ namespace osu.Game.Rulesets.Scoring
/// <list type="bullet"> /// <list type="bullet">
/// <item>The current <see cref="ScoringValues.MaxCombo"/> will always be 0.</item> /// <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 maximum <see cref="ScoringValues.BonusScore"/> will always be 0.</item>
/// <item>The current and maximum <see cref="ScoringValues.HitObjects"/> will always be the same value.</item> /// <item>The current and maximum <see cref="ScoringValues.CountBasicHitObjects"/> will always be the same value.</item>
/// </list> /// </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"/>). /// 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> /// </remarks>
@ -573,8 +582,8 @@ namespace osu.Game.Rulesets.Scoring
if (result.IsBasic()) if (result.IsBasic())
{ {
current.HitObjects += count; current.CountBasicHitObjects += count;
maximum.HitObjects += count; maximum.CountBasicHitObjects += count;
} }
} }
} }

View File

@ -8,6 +8,9 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.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] [MessagePackObject]
public struct ScoringValues public struct ScoringValues
{ {
@ -33,6 +36,6 @@ namespace osu.Game.Scoring
/// The count of "basic" <see cref="HitObject"/>s. See: <see cref="HitResultExtensions.IsBasic"/>. /// The count of "basic" <see cref="HitObject"/>s. See: <see cref="HitResultExtensions.IsBasic"/>.
/// </summary> /// </summary>
[Key(3)] [Key(3)]
public int HitObjects; public int CountBasicHitObjects;
} }
} }

View File

@ -2,13 +2,16 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit namespace osu.Game.Screens.Edit
{ {
@ -26,6 +29,14 @@ namespace osu.Game.Screens.Edit
Height = 60; Height = 60;
Masking = true;
EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.Black.Opacity(0.2f),
Type = EdgeEffectType.Shadow,
Radius = 10f,
};
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new Box new Box

View File

@ -99,6 +99,15 @@ namespace osu.Game.Screens.Edit
colourSelected = colours.Colour3; colourSelected = colours.Colour3;
} }
protected override void LoadComplete()
{
base.LoadComplete();
// Reduce flicker of rows when offset is being changed rapidly.
// Probably need to reconsider this.
FinishTransforms(true);
}
private bool selected; private bool selected;
public bool Selected public bool Selected

View File

@ -61,6 +61,7 @@ namespace osu.Game.Screens.Edit.Timing
selectedGroup.BindValueChanged(group => selectedGroup.BindValueChanged(group =>
{ {
// TODO: This should scroll the selected row into view.
foreach (var b in BackgroundFlow) b.Selected = b.Item == group.NewValue; foreach (var b in BackgroundFlow) b.Selected = b.Item == group.NewValue;
}, true); }, true);
} }

View File

@ -31,18 +31,33 @@ namespace osu.Game.Screens.Edit.Timing
}); });
} }
protected override void LoadComplete()
{
base.LoadComplete();
kiai.Current.BindValueChanged(_ => saveChanges());
omitBarLine.Current.BindValueChanged(_ => saveChanges());
scrollSpeedSlider.Current.BindValueChanged(_ => saveChanges());
void saveChanges()
{
if (!isRebinding) ChangeHandler?.SaveState();
}
}
private bool isRebinding;
protected override void OnControlPointChanged(ValueChangedEvent<EffectControlPoint> point) protected override void OnControlPointChanged(ValueChangedEvent<EffectControlPoint> point)
{ {
if (point.NewValue != null) if (point.NewValue != null)
{ {
isRebinding = true;
kiai.Current = point.NewValue.KiaiModeBindable; kiai.Current = point.NewValue.KiaiModeBindable;
kiai.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable; omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
omitBarLine.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
scrollSpeedSlider.Current = point.NewValue.ScrollSpeedBindable; scrollSpeedSlider.Current = point.NewValue.ScrollSpeedBindable;
scrollSpeedSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
isRebinding = false;
} }
} }

View File

@ -3,6 +3,8 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Extensions.LocalisationExtensions;
@ -31,12 +33,16 @@ namespace osu.Game.Screens.Edit.Timing
private IAdjustableClock metronomeClock; private IAdjustableClock metronomeClock;
private Sample clunk;
[Resolved] [Resolved]
private OverlayColourProvider overlayColourProvider { get; set; } private OverlayColourProvider overlayColourProvider { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(AudioManager audio)
{ {
clunk = audio.Samples.Get(@"Multiplayer/countdown-tick");
const float taper = 25; const float taper = 25;
const float swing_vertical_offset = -23; const float swing_vertical_offset = -23;
const float lower_cover_height = 32; const float lower_cover_height = 32;
@ -269,8 +275,21 @@ namespace osu.Game.Screens.Edit.Timing
if (currentAngle != 0 && Math.Abs(currentAngle - targetAngle) > angle * 1.8f && isSwinging) if (currentAngle != 0 && Math.Abs(currentAngle - targetAngle) > angle * 1.8f && isSwinging)
{ {
using (stick.BeginDelayedSequence(beatLength / 2)) using (BeginDelayedSequence(beatLength / 2))
{
stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint); stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint);
Schedule(() =>
{
var channel = clunk?.GetChannel();
if (channel != null)
{
channel.Frequency.Value = RNG.NextDouble(0.98f, 1.02f);
channel.Play();
}
});
}
} }
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -18,6 +19,9 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved] [Resolved]
private EditorClock editorClock { get; set; } private EditorClock editorClock { get; set; }
[Resolved]
private EditorBeatmap beatmap { get; set; }
[Resolved] [Resolved]
private Bindable<ControlPointGroup> selectedGroup { get; set; } private Bindable<ControlPointGroup> selectedGroup { get; set; }
@ -45,6 +49,7 @@ namespace osu.Game.Screens.Edit.Timing
{ {
new Dimension(GridSizeMode.Absolute, 200), new Dimension(GridSizeMode.Absolute, 200),
new Dimension(GridSizeMode.Absolute, 60), new Dimension(GridSizeMode.Absolute, 60),
new Dimension(GridSizeMode.Absolute, 60),
}, },
Content = new[] Content = new[]
{ {
@ -77,7 +82,36 @@ namespace osu.Game.Screens.Edit.Timing
}, },
} }
} }
} },
},
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(10),
Children = new Drawable[]
{
new TimingAdjustButton(1)
{
Text = "Offset",
RelativeSizeAxes = Axes.X,
Width = 0.48f,
Height = 50,
Action = adjustOffset,
},
new TimingAdjustButton(0.1)
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Text = "BPM",
RelativeSizeAxes = Axes.X,
Width = 0.48f,
Height = 50,
Action = adjustBpm,
}
}
},
}, },
new Drawable[] new Drawable[]
{ {
@ -113,6 +147,35 @@ namespace osu.Game.Screens.Edit.Timing
}; };
} }
private void adjustOffset(double adjust)
{
// VERY TEMPORARY
var currentGroupItems = selectedGroup.Value.ControlPoints.ToArray();
beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value);
double newOffset = selectedGroup.Value.Time + adjust;
foreach (var cp in currentGroupItems)
beatmap.ControlPointInfo.Add(newOffset, cp);
// the control point might not necessarily exist yet, if currentGroupItems was empty.
selectedGroup.Value = beatmap.ControlPointInfo.GroupAt(newOffset, true);
if (!editorClock.IsRunning)
editorClock.Seek(newOffset);
}
private void adjustBpm(double adjust)
{
var timing = selectedGroup.Value.ControlPoints.OfType<TimingControlPoint>().FirstOrDefault();
if (timing == null)
return;
timing.BeatLength = 60000 / (timing.BPM + adjust);
}
private void tap() private void tap()
{ {
editorClock.Seek(selectedGroup.Value.Time); editorClock.Seek(selectedGroup.Value.Time);

View File

@ -0,0 +1,254 @@
// 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.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;
namespace osu.Game.Screens.Edit.Timing
{
/// <summary>
/// A button with variable constant output based on hold position and length.
/// </summary>
public class TimingAdjustButton : CompositeDrawable
{
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;
}
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
public TimingAdjustButton(double adjustAmount)
{
this.adjustAmount = adjustAmount;
CornerRadius = 5;
Masking = true;
AddInternal(Content = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue
},
text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Default.With(weight: FontWeight.SemiBold),
Padding = new MarginPadding(5),
Depth = float.MinValue
}
}
});
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
sample = audio.Samples.Get(@"UI/notch-tick");
background.Colour = colourProvider.Background3;
for (int i = 1; i <= adjust_levels; i++)
{
Content.Add(new IncrementBox(i, adjustAmount));
Content.Add(new IncrementBox(-i, adjustAmount));
}
}
protected override bool OnMouseDown(MouseDownEvent e)
{
beginRepeat();
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;
private readonly Box box;
private readonly OsuSpriteText text;
public IncrementBox(int index, double amount)
{
Multiplier = Math.Sign(index) * convertMultiplier(index);
float ratio = (float)index / adjust_levels;
RelativeSizeAxes = Axes.Both;
Width = 0.5f * Math.Abs(ratio);
Anchor direction = index < 0 ? Anchor.x2 : Anchor.x0;
Origin |= direction;
Depth = Math.Abs(index);
Anchor = Anchor.TopCentre;
InternalChildren = new Drawable[]
{
box = new Box
{
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive
},
text = new OsuSpriteText
{
Anchor = direction,
Origin = direction,
Font = OsuFont.Default.With(size: 10, weight: FontWeight.Bold),
Text = $"{(index > 0 ? "+" : "-")}{Math.Abs(Multiplier * amount)}",
Padding = new MarginPadding(5),
Alpha = 0,
}
};
}
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
box.Colour = colourProvider.Background1;
box.Alpha = 0.1f;
}
private float convertMultiplier(int m)
{
switch (Math.Abs(m))
{
default: return 1;
case 2: return 2;
case 3: return 5;
case 4:
return max_multiplier;
}
}
protected override bool OnHover(HoverEvent e)
{
box.Colour = colourProvider.Colour0;
box.FadeTo(0.2f, 100, Easing.OutQuint);
text.FadeIn(100, Easing.OutQuint);
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
box.Colour = colourProvider.Background1;
box.FadeTo(0.1f, 500, Easing.OutQuint);
text.FadeOut(100, Easing.OutQuint);
base.OnHoverLost(e);
}
public void Flash()
{
box
.FadeTo(0.4f, 20, Easing.OutQuint)
.Then()
.FadeTo(0.2f, 400, Easing.OutQuint);
text
.MoveToY(-5, 20, Easing.OutQuint)
.Then()
.MoveToY(0, 400, Easing.OutQuint);
}
}
}
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Screens.Edit.Timing
public class TimingScreen : EditorScreenWithTimeline public class TimingScreen : EditorScreenWithTimeline
{ {
[Cached] [Cached]
private Bindable<ControlPointGroup> selectedGroup = new Bindable<ControlPointGroup>(); public readonly Bindable<ControlPointGroup> SelectedGroup = new Bindable<ControlPointGroup>();
public TimingScreen() public TimingScreen()
: base(EditorScreenMode.Timing) : base(EditorScreenMode.Timing)
@ -132,6 +132,40 @@ namespace osu.Game.Screens.Edit.Timing
}, true); }, true);
} }
protected override void Update()
{
base.Update();
trackActivePoint();
}
/// <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.
///
/// So if the user is currently looking at a timing point and seeks into the future, a
/// future timing point would be automatically selected if it is now the new "current" point.
/// </summary>
private void trackActivePoint()
{
// For simplicity only match on the first type of the active control point.
var selectedPointType = selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType();
if (selectedPointType != 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))
.LastOrDefault(g => g.Time <= clock.CurrentTimeAccurate);
if (found != null)
selectedGroup.Value = found;
}
}
private void delete() private void delete()
{ {
if (selectedGroup.Value == null) if (selectedGroup.Value == null)
@ -144,7 +178,27 @@ namespace osu.Game.Screens.Edit.Timing
private void addNew() private void addNew()
{ {
selectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true); bool isFirstControlPoint = !Beatmap.ControlPointInfo.TimingPoints.Any();
var group = Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true);
if (isFirstControlPoint)
group.Add(new TimingControlPoint());
else
{
// Try and create matching types from the currently selected control point.
var selected = selectedGroup.Value;
if (selected != null)
{
foreach (var controlPoint in selected.ControlPoints)
{
group.Add(controlPoint.DeepClone());
}
}
}
selectedGroup.Value = group;
} }
} }
} }

View File

@ -28,15 +28,31 @@ namespace osu.Game.Screens.Edit.Timing
}); });
} }
protected override void LoadComplete()
{
base.LoadComplete();
bpmTextEntry.Current.BindValueChanged(_ => saveChanges());
timeSignature.Current.BindValueChanged(_ => saveChanges());
void saveChanges()
{
if (!isRebinding) ChangeHandler?.SaveState();
}
}
private bool isRebinding;
protected override void OnControlPointChanged(ValueChangedEvent<TimingControlPoint> point) protected override void OnControlPointChanged(ValueChangedEvent<TimingControlPoint> point)
{ {
if (point.NewValue != null) if (point.NewValue != null)
{ {
bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable; isRebinding = true;
bpmTextEntry.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable;
timeSignature.Current = point.NewValue.TimeSignatureBindable; timeSignature.Current = point.NewValue.TimeSignatureBindable;
timeSignature.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
isRebinding = false;
} }
} }

View File

@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -26,6 +27,8 @@ namespace osu.Game.Screens.Edit.Timing
{ {
private const int total_waveforms = 8; private const int total_waveforms = 8;
private const float corner_radius = LabelledDrawable<Drawable>.CORNER_RADIUS;
private readonly BindableNumber<double> beatLength = new BindableDouble(); private readonly BindableNumber<double> beatLength = new BindableDouble();
[Resolved] [Resolved]
@ -42,18 +45,22 @@ namespace osu.Game.Screens.Edit.Timing
private TimingControlPoint timingPoint = TimingControlPoint.DEFAULT; private TimingControlPoint timingPoint = TimingControlPoint.DEFAULT;
private int lastDisplayedBeatIndex; private double displayedTime;
private double selectedGroupStartTime; private double selectedGroupStartTime;
private double selectedGroupEndTime; private double selectedGroupEndTime;
private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>(); private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
private readonly BindableBool displayLocked = new BindableBool();
private LockedOverlay lockedOverlay = null!;
public WaveformComparisonDisplay() public WaveformComparisonDisplay()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
CornerRadius = LabelledDrawable<Drawable>.CORNER_RADIUS; CornerRadius = corner_radius;
Masking = true; Masking = true;
} }
@ -63,7 +70,7 @@ namespace osu.Game.Screens.Edit.Timing
for (int i = 0; i < total_waveforms; i++) for (int i = 0; i < total_waveforms; i++)
{ {
AddInternal(new WaveformRow AddInternal(new WaveformRow(i == total_waveforms / 2)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both, RelativePositionAxes = Axes.Both,
@ -81,72 +88,112 @@ namespace osu.Game.Screens.Edit.Timing
Width = 3, Width = 3,
}); });
AddInternal(lockedOverlay = new LockedOverlay());
selectedGroup.BindValueChanged(_ => updateTimingGroup(), true); selectedGroup.BindValueChanged(_ => updateTimingGroup(), true);
controlPointGroups.BindTo(editorBeatmap.ControlPointInfo.Groups); controlPointGroups.BindTo(editorBeatmap.ControlPointInfo.Groups);
controlPointGroups.BindCollectionChanged((_, __) => updateTimingGroup()); controlPointGroups.BindCollectionChanged((_, __) => updateTimingGroup());
beatLength.BindValueChanged(_ => showFrom(lastDisplayedBeatIndex), true); beatLength.BindValueChanged(_ => regenerateDisplay(true), true);
displayLocked.BindValueChanged(locked =>
{
if (locked.NewValue)
lockedOverlay.Show();
else
lockedOverlay.Hide();
}, true);
} }
private void updateTimingGroup() private void updateTimingGroup()
{ {
beatLength.UnbindBindings(); beatLength.UnbindBindings();
selectedGroupStartTime = 0;
selectedGroupEndTime = beatmap.Value.Track.Length;
var tcp = selectedGroup.Value?.ControlPoints.OfType<TimingControlPoint>().FirstOrDefault(); var tcp = selectedGroup.Value?.ControlPoints.OfType<TimingControlPoint>().FirstOrDefault();
if (tcp == null) if (tcp == null)
{ {
timingPoint = new TimingControlPoint(); timingPoint = new TimingControlPoint();
// During movement of a control point's offset, this clause can be hit momentarily,
// as moving a control point is implemented by removing it and inserting it at the new time.
// We don't want to reset the `selectedGroupStartTime` here as we rely on having the
// last value to update the waveform display below.
selectedGroupEndTime = beatmap.Value.Track.Length;
return; return;
} }
timingPoint = tcp; timingPoint = tcp;
beatLength.BindTo(timingPoint.BeatLengthBindable); beatLength.BindTo(timingPoint.BeatLengthBindable);
selectedGroupStartTime = selectedGroup.Value?.Time ?? 0; double? newStartTime = selectedGroup.Value?.Time;
double? offsetChange = newStartTime - selectedGroupStartTime;
var nextGroup = editorBeatmap.ControlPointInfo.TimingPoints var nextGroup = editorBeatmap.ControlPointInfo.TimingPoints
.SkipWhile(g => g != tcp) .SkipWhile(g => g != tcp)
.Skip(1) .Skip(1)
.FirstOrDefault(); .FirstOrDefault();
if (nextGroup != null) selectedGroupStartTime = newStartTime ?? 0;
selectedGroupEndTime = nextGroup.Time; selectedGroupEndTime = nextGroup?.Time ?? beatmap.Value.Track.Length;
if (newStartTime.HasValue && offsetChange.HasValue)
{
// The offset of the selected point may have changed.
// This handles the case the user has locked the view and expects the display to update with this change.
showFromTime(displayedTime + offsetChange.Value, true);
}
} }
protected override bool OnHover(HoverEvent e) => true; protected override bool OnHover(HoverEvent e) => true;
protected override bool OnMouseMove(MouseMoveEvent e) protected override bool OnMouseMove(MouseMoveEvent e)
{ {
float trackLength = (float)beatmap.Value.Track.Length; if (!displayLocked.Value)
int totalBeatsAvailable = (int)(trackLength / timingPoint.BeatLength); {
float trackLength = (float)beatmap.Value.Track.Length;
int totalBeatsAvailable = (int)(trackLength / timingPoint.BeatLength);
Scheduler.AddOnce(showFrom, (int)(e.MousePosition.X / DrawWidth * totalBeatsAvailable)); Scheduler.AddOnce(showFromBeat, (int)(e.MousePosition.X / DrawWidth * totalBeatsAvailable));
}
return base.OnMouseMove(e); return base.OnMouseMove(e);
} }
protected override bool OnClick(ClickEvent e)
{
displayLocked.Toggle();
return true;
}
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
if (!IsHovered) if (!IsHovered && !displayLocked.Value)
{ {
int currentBeat = (int)Math.Floor((editorClock.CurrentTimeAccurate - selectedGroupStartTime) / timingPoint.BeatLength); int currentBeat = (int)Math.Floor((editorClock.CurrentTimeAccurate - selectedGroupStartTime) / timingPoint.BeatLength);
showFrom(currentBeat); showFromBeat(currentBeat);
} }
} }
private void showFrom(int beatIndex) private void showFromBeat(int beatIndex) =>
showFromTime(selectedGroupStartTime + beatIndex * timingPoint.BeatLength, false);
private void showFromTime(double time, bool animated)
{ {
if (lastDisplayedBeatIndex == beatIndex) if (displayedTime == time)
return; return;
displayedTime = time;
regenerateDisplay(animated);
}
private void regenerateDisplay(bool animated)
{
double index = (displayedTime - selectedGroupStartTime) / timingPoint.BeatLength;
// Chosen as a pretty usable number across all BPMs. // Chosen as a pretty usable number across all BPMs.
// Optimally we'd want this to scale with the BPM in question, but performing // Optimally we'd want this to scale with the BPM in question, but performing
// scaling of the display is both expensive in resampling, and decreases usability // scaling of the display is both expensive in resampling, and decreases usability
@ -156,38 +203,115 @@ namespace osu.Game.Screens.Edit.Timing
float trackLength = (float)beatmap.Value.Track.Length; float trackLength = (float)beatmap.Value.Track.Length;
float scale = trackLength / visible_width; float scale = trackLength / visible_width;
const int start_offset = total_waveforms / 2;
// Start displaying from before the current beat // Start displaying from before the current beat
beatIndex -= total_waveforms / 2; index -= start_offset;
foreach (var row in InternalChildren.OfType<WaveformRow>()) foreach (var row in InternalChildren.OfType<WaveformRow>())
{ {
// offset to the required beat index. // offset to the required beat index.
double time = selectedGroupStartTime + beatIndex * timingPoint.BeatLength; double time = selectedGroupStartTime + index * timingPoint.BeatLength;
float offset = (float)(time - visible_width / 2) / trackLength * scale; float offset = (float)(time - visible_width / 2) / trackLength * scale;
row.Alpha = time < selectedGroupStartTime || time > selectedGroupEndTime ? 0.2f : 1; row.Alpha = time < selectedGroupStartTime || time > selectedGroupEndTime ? 0.2f : 1;
row.WaveformOffset = -offset; row.WaveformOffsetTo(-offset, animated);
row.WaveformScale = new Vector2(scale, 1); row.WaveformScale = new Vector2(scale, 1);
row.BeatIndex = beatIndex++; row.BeatIndex = (int)Math.Floor(index);
index++;
}
}
internal class LockedOverlay : CompositeDrawable
{
private OsuSpriteText text = null!;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.Both;
Masking = true;
CornerRadius = corner_radius;
BorderColour = colours.Red;
BorderThickness = 3;
Alpha = 0;
InternalChildren = new Drawable[]
{
new Box
{
AlwaysPresent = true,
RelativeSizeAxes = Axes.Both,
Alpha = 0,
},
new Container
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = colours.Red,
RelativeSizeAxes = Axes.Both,
},
text = new OsuSpriteText
{
Colour = colours.GrayF,
Text = "Locked",
Margin = new MarginPadding(5),
Shadow = false,
Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold),
}
}
},
};
} }
lastDisplayedBeatIndex = beatIndex; public override void Show()
{
this.FadeIn(100, Easing.OutQuint);
text
.FadeIn().Then().Delay(600)
.FadeOut().Then().Delay(600)
.Loop();
}
public override void Hide()
{
this.FadeOut(100, Easing.OutQuint);
}
} }
internal class WaveformRow : CompositeDrawable internal class WaveformRow : CompositeDrawable
{ {
private readonly bool isMainRow;
private OsuSpriteText beatIndexText = null!; private OsuSpriteText beatIndexText = null!;
private WaveformGraph waveformGraph = null!; private WaveformGraph waveformGraph = null!;
[Resolved] [Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!; private OverlayColourProvider colourProvider { get; set; } = null!;
public WaveformRow(bool isMainRow)
{
this.isMainRow = isMainRow;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap) private void load(IBindable<WorkingBeatmap> beatmap)
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new Box
{
Colour = colourProvider.Background3,
Alpha = isMainRow ? 1 : 0,
RelativeSizeAxes = Axes.Both,
},
waveformGraph = new WaveformGraph waveformGraph = new WaveformGraph
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -212,7 +336,15 @@ namespace osu.Game.Screens.Edit.Timing
public int BeatIndex { set => beatIndexText.Text = value.ToString(); } public int BeatIndex { set => beatIndexText.Text = value.ToString(); }
public Vector2 WaveformScale { set => waveformGraph.Scale = value; } public Vector2 WaveformScale { set => waveformGraph.Scale = value; }
public float WaveformOffset { set => waveformGraph.X = value; }
public void WaveformOffsetTo(float value, bool animated) =>
this.TransformTo(nameof(waveformOffset), value, animated ? 300 : 0, Easing.OutQuint);
private float waveformOffset
{
get => waveformGraph.X;
set => waveformGraph.X = value;
}
} }
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -67,7 +68,25 @@ namespace osu.Game.Tests.Visual.OnlinePlay
// To get around this, the BeatmapManager is looked up from the dependencies provided to the children of the test scene instead. // To get around this, the BeatmapManager is looked up from the dependencies provided to the children of the test scene instead.
var beatmapManager = dependencies.Get<BeatmapManager>(); var beatmapManager = dependencies.Get<BeatmapManager>();
((DummyAPIAccess)API).HandleRequest = request => handler.HandleRequest(request, API.LocalUser.Value, beatmapManager); ((DummyAPIAccess)API).HandleRequest = request =>
{
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
// Because some of the handlers use realm, we need to ensure the game is still alive when firing.
// If we don't, a stray `PerformAsync` could hit an `ObjectDisposedException` if running too late.
Scheduler.Add(() =>
{
bool result = handler.HandleRequest(request, API.LocalUser.Value, beatmapManager);
tcs.SetResult(result);
}, false);
#pragma warning disable RS0030
// We can't GetResultSafely() here (will fail with "Can't use GetResultSafely from inside an async operation."), but Wait is safe enough due to
// the task being a TaskCompletionSource.
// Importantly, this doesn't deadlock because of the scheduler call above running inline where feasible (see the `false` argument).
return tcs.Task.Result;
#pragma warning restore RS0030
};
}); });
/// <summary> /// <summary>

View File

@ -48,9 +48,8 @@ namespace osu.Game.Utils
options.AutoSessionTracking = true; options.AutoSessionTracking = true;
options.IsEnvironmentUser = false; options.IsEnvironmentUser = false;
// The reported release needs to match release tags on github in order for sentry // The reported release needs to match version as reported to Sentry in .github/workflows/sentry-release.yml
// to automatically associate and track against releases. options.Release = $"osu@{game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty)}";
options.Release = game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty);
}); });
Logger.NewEntry += processLogEntry; Logger.NewEntry += processLogEntry;