1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 17:32:54 +08:00

Merge branch 'master' into skin-bindables

This commit is contained in:
Bartłomiej Dach 2021-05-05 20:16:27 +02:00 committed by GitHub
commit 3cac837acf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 569 additions and 97 deletions

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
Entry = null;
}
private void onEntryInvalidated() => refreshPoints();
private void onEntryInvalidated() => Scheduler.AddOnce(refreshPoints);
private void refreshPoints()
{

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Replays;
using osu.Game.Rulesets.Replays;
@ -278,6 +279,54 @@ namespace osu.Game.Tests.NonVisual
setTime(-100, -100);
}
[Test]
public void TestReplayFramesSortStability()
{
const double repeating_time = 5000;
// add a collection of frames in shuffled order time-wise; each frame also stores its original index to check stability later.
// data is hand-picked and breaks if the unstable List<T>.Sort() is used.
// in theory this can still return a false-positive with another unstable algorithm if extremely unlucky,
// but there is no conceivable fool-proof way to prevent that anyways.
replay.Frames.AddRange(new[]
{
repeating_time,
0,
3000,
repeating_time,
repeating_time,
6000,
9000,
repeating_time,
repeating_time,
1000,
11000,
21000,
4000,
repeating_time,
repeating_time,
8000,
2000,
7000,
repeating_time,
repeating_time,
10000
}.Select((time, index) => new TestReplayFrame(time, true, index)));
replay.HasReceivedAllFrames = true;
// create a new handler with the replay for the sort to be performed.
handler = new TestInputHandler(replay);
// ensure sort stability by checking that the frames with time == repeating_time are sorted in ascending frame index order themselves.
var repeatingTimeFramesData = replay.Frames
.Cast<TestReplayFrame>()
.Where(f => f.Time == repeating_time)
.Select(f => f.FrameIndex);
Assert.That(repeatingTimeFramesData, Is.Ordered.Ascending);
}
private void setReplayFrames()
{
replay.Frames = new List<ReplayFrame>
@ -324,11 +373,13 @@ namespace osu.Game.Tests.NonVisual
private class TestReplayFrame : ReplayFrame
{
public readonly bool IsImportant;
public readonly int FrameIndex;
public TestReplayFrame(double time, bool isImportant = false)
public TestReplayFrame(double time, bool isImportant = false, int frameIndex = 0)
: base(time)
{
IsImportant = isImportant;
FrameIndex = frameIndex;
}
}

View File

@ -0,0 +1,191 @@
// 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 System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Storyboards;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneStoryboardWithOutro : PlayerTestScene
{
protected override bool HasCustomSteps => true;
protected new OutroPlayer Player => (OutroPlayer)base.Player;
private double currentStoryboardDuration;
private bool showResults = true;
private event Func<HealthProcessor, JudgementResult, bool> currentFailConditions;
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("enable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("set dim level to 0", () => LocalConfig.SetValue<double>(OsuSetting.DimLevel, 0));
AddStep("reset fail conditions", () => currentFailConditions = (_, __) => false);
AddStep("set storyboard duration to 2s", () => currentStoryboardDuration = 2000);
AddStep("set ShowResults = true", () => showResults = true);
}
[Test]
public void TestStoryboardSkipOutro()
{
CreateTest(null);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("skip outro", () => InputManager.Key(osuTK.Input.Key.Space));
AddAssert("score shown", () => Player.IsScoreShown);
}
[Test]
public void TestStoryboardNoSkipOutro()
{
CreateTest(null);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
}
[Test]
public void TestStoryboardExitToSkipOutro()
{
CreateTest(null);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("exit via pause", () => Player.ExitViaPause());
AddAssert("score shown", () => Player.IsScoreShown);
}
[TestCase(false)]
[TestCase(true)]
public void TestStoryboardToggle(bool enabledAtBeginning)
{
CreateTest(null);
AddStep($"{(enabledAtBeginning ? "enable" : "disable")} storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, enabledAtBeginning));
AddStep("toggle storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, !enabledAtBeginning));
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
}
[Test]
public void TestOutroEndsDuringFailAnimation()
{
CreateTest(() =>
{
AddStep("fail on first judgement", () => currentFailConditions = (_, __) => true);
AddStep("set storyboard duration to 1.3s", () => currentStoryboardDuration = 1300);
});
AddUntilStep("wait for fail", () => Player.HasFailed);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
}
[Test]
public void TestShowResultsFalse()
{
CreateTest(() =>
{
AddStep("set ShowResults = false", () => showResults = false);
});
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddWaitStep("wait", 10);
AddAssert("no score shown", () => !Player.IsScoreShown);
}
[Test]
public void TestStoryboardEndsBeforeCompletion()
{
CreateTest(() => AddStep("set storyboard duration to .1s", () => currentStoryboardDuration = 100));
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
}
[Test]
public void TestStoryboardRewind()
{
SkipOverlay.FadeContainer fadeContainer() => Player.ChildrenOfType<SkipOverlay.FadeContainer>().First();
CreateTest(null);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddUntilStep("skip overlay content becomes visible", () => fadeContainer().State == Visibility.Visible);
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-1000));
AddUntilStep("skip overlay content not visible", () => fadeContainer().State == Visibility.Hidden);
AddUntilStep("skip overlay content becomes visible", () => fadeContainer().State == Visibility.Visible);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
}
protected override bool AllowFail => true;
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new OutroPlayer(currentFailConditions, showResults);
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap();
beatmap.HitObjects.Add(new HitCircle());
return beatmap;
}
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{
return base.CreateWorkingBeatmap(beatmap, createStoryboard(currentStoryboardDuration));
}
private Storyboard createStoryboard(double duration)
{
var storyboard = new Storyboard();
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
sprite.TimelineGroup.Alpha.Add(Easing.None, 0, duration, 1, 0);
storyboard.GetLayer("Background").Add(sprite);
return storyboard;
}
protected class OutroPlayer : TestPlayer
{
public void ExitViaPause() => PerformExit(true);
public new FailOverlay FailOverlay => base.FailOverlay;
public bool IsScoreShown => !this.IsCurrentScreen() && this.GetChildScreen() is ResultsScreen;
private event Func<HealthProcessor, JudgementResult, bool> failConditions;
public OutroPlayer(Func<HealthProcessor, JudgementResult, bool> failConditions, bool showResults = true)
: base(false, showResults)
{
this.failConditions = failConditions;
}
protected override void LoadComplete()
{
base.LoadComplete();
HealthProcessor.FailConditions += failConditions;
}
protected override Task ImportScore(Score score)
{
return Task.CompletedTask;
}
}
}
}

View File

@ -7,6 +7,7 @@ using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
@ -112,8 +113,8 @@ namespace osu.Game.Tests.Visual.SongSelect
private void testInfoLabels(int expectedCount)
{
AddAssert("check info labels exists", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Any());
AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Count() == expectedCount);
AddAssert("check info labels exists", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Count() == expectedCount);
}
[Test]
@ -124,7 +125,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("check default title", () => infoWedge.Info.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title);
AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist);
AddAssert("check empty author", () => !infoWedge.Info.MapperContainer.Children.Any());
AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Any());
AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
}
[Test]
@ -135,15 +136,15 @@ namespace osu.Game.Tests.Visual.SongSelect
private void selectBeatmap([CanBeNull] IBeatmap b)
{
BeatmapInfoWedge.BufferedWedgeInfo infoBefore = null;
Container containerBefore = null;
AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
{
infoBefore = infoWedge.Info;
containerBefore = infoWedge.DisplayedContent;
infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b);
});
AddUntilStep("wait for async load", () => infoWedge.Info != infoBefore);
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
}
private IBeatmap createTestBeatmap(RulesetInfo ruleset)
@ -193,7 +194,9 @@ namespace osu.Game.Tests.Visual.SongSelect
private class TestBeatmapInfoWedge : BeatmapInfoWedge
{
public new BufferedWedgeInfo Info => base.Info;
public new Container DisplayedContent => base.DisplayedContent;
public new WedgeInfoText Info => base.Info;
}
private class TestHitObject : ConvertHitObject, IHasPosition

View File

@ -28,7 +28,7 @@ namespace osu.Game.Graphics.Containers
protected override bool BlockNonPositionalInput => true;
/// <summary>
/// Temporary to allow for overlays in the main screen content to not dim theirselves.
/// Temporary to allow for overlays in the main screen content to not dim themselves.
/// Should be eventually replaced by dimming which is aware of the target dim container (traverse parent for certain interface type?).
/// </summary>
protected virtual bool DimMainContent => true;

View File

@ -141,7 +141,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
scalingSettings.ForEach(s => bindPreviewEvent(s.Current));
windowModeDropdown.Current.ValueChanged += _ => updateResolutionDropdown();
windowModeDropdown.Current.BindValueChanged(mode =>
{
updateResolutionDropdown();
const string not_fullscreen_note = "Running without fullscreen mode will increase your input latency!";
windowModeDropdown.WarningText = mode.NewValue != WindowMode.Fullscreen ? not_fullscreen_note : string.Empty;
}, true);
windowModes.BindCollectionChanged((sender, args) =>
{

View File

@ -13,6 +13,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
{
protected override string Header => "Renderer";
private SettingsEnumDropdown<FrameSync> frameLimiterDropdown;
[BackgroundDependencyLoader]
private void load(FrameworkConfigManager config, OsuConfigManager osuConfig)
{
@ -20,7 +22,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
Children = new Drawable[]
{
// TODO: this needs to be a custom dropdown at some point
new SettingsEnumDropdown<FrameSync>
frameLimiterDropdown = new SettingsEnumDropdown<FrameSync>
{
LabelText = "Frame limiter",
Current = config.GetBindable<FrameSync>(FrameworkSetting.FrameSync)
@ -37,5 +39,17 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
frameLimiterDropdown.Current.BindValueChanged(limit =>
{
const string unlimited_frames_note = "Using unlimited frame limiter can lead to stutters, bad performance and overheating. It will not improve perceived latency. \"2x refresh rate\" is recommended.";
frameLimiterDropdown.WarningText = limit.NewValue == FrameSync.Unlimited ? unlimited_frames_note : string.Empty;
}, true);
}
}
}

View File

@ -2,8 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Users;
namespace osu.Game.Overlays.Settings.Sections.UserInterface
{
@ -11,9 +14,15 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
{
protected override string Header => "Main Menu";
private IBindable<User> user;
private SettingsEnumDropdown<BackgroundSource> backgroundSourceDropdown;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
private void load(OsuConfigManager config, IAPIProvider api)
{
user = api.LocalUser.GetBoundCopy();
Children = new Drawable[]
{
new SettingsCheckbox
@ -31,7 +40,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
LabelText = "Intro sequence",
Current = config.GetBindable<IntroSequence>(OsuSetting.IntroSequence),
},
new SettingsEnumDropdown<BackgroundSource>
backgroundSourceDropdown = new SettingsEnumDropdown<BackgroundSource>
{
LabelText = "Background source",
Current = config.GetBindable<BackgroundSource>(OsuSetting.MenuBackgroundSource),
@ -43,5 +52,17 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
backgroundSourceDropdown.Current.BindValueChanged(source =>
{
const string not_supporter_note = "Changes to this setting will only apply with an active osu!supporter tag.";
backgroundSourceDropdown.WarningText = user.Value?.IsSupporter != true ? not_supporter_note : string.Empty;
}, true);
}
}
}

View File

@ -18,7 +18,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.Settings
{
@ -36,10 +36,15 @@ namespace osu.Game.Overlays.Settings
private SpriteText labelText;
private OsuTextFlowContainer warningText;
public bool ShowsDefaultIndicator = true;
public string TooltipText { get; set; }
[Resolved]
private OsuColour colours { get; set; }
public virtual LocalisableString LabelText
{
get => labelText?.Text ?? string.Empty;
@ -57,6 +62,31 @@ namespace osu.Game.Overlays.Settings
}
}
/// <summary>
/// Text to be displayed at the bottom of this <see cref="SettingsItem{T}"/>.
/// Generally used to recommend the user change their setting as the current one is considered sub-optimal.
/// </summary>
public string WarningText
{
set
{
if (warningText == null)
{
// construct lazily for cases where the label is not needed (may be provided by the Control).
FlowContent.Add(warningText = new OsuTextFlowContainer
{
Colour = colours.Yellow,
Margin = new MarginPadding { Bottom = 5 },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
});
}
warningText.Alpha = string.IsNullOrWhiteSpace(value) ? 0 : 1;
warningText.Text = value;
}
}
public virtual Bindable<T> Current
{
get => controlWithCurrent.Current;
@ -92,7 +122,10 @@ namespace osu.Game.Overlays.Settings
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS },
Child = Control = CreateControl()
Children = new[]
{
Control = CreateControl(),
},
},
};
@ -141,6 +174,7 @@ namespace osu.Game.Overlays.Settings
{
RelativeSizeAxes = Axes.Y;
Width = SettingsPanel.CONTENT_MARGINS;
Padding = new MarginPadding { Vertical = 1.5f };
Alpha = 0f;
}
@ -163,7 +197,7 @@ namespace osu.Game.Overlays.Settings
Type = EdgeEffectType.Glow,
Radius = 2,
},
Size = new Vector2(0.33f, 0.8f),
Width = 0.33f,
Child = new Box { RelativeSizeAxes = Axes.Both },
};
}
@ -196,12 +230,6 @@ namespace osu.Game.Overlays.Settings
UpdateState();
}
public void SetButtonColour(Color4 buttonColour)
{
this.buttonColour = buttonColour;
UpdateState();
}
public void UpdateState() => Scheduler.AddOnce(updateState);
private void updateState()

View File

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Game.Input.Handlers;
using osu.Game.Replays;
@ -97,7 +98,7 @@ namespace osu.Game.Rulesets.Replays
{
// TODO: This replay frame ordering should be enforced on the Replay type.
// Currently, the ordering can be broken if the frames are added after this construction.
replay.Frames.Sort((x, y) => x.Time.CompareTo(y.Time));
replay.Frames = replay.Frames.OrderBy(f => f.Time).ToList();
this.replay = replay;
currentFrameIndex = -1;

View File

@ -34,13 +34,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader]
private void load()
{
// For non-pooled rulesets, hitobjects are already present in the playfield which allows the blueprints to be loaded in the async context.
if (Composer != null)
{
foreach (var obj in Composer.HitObjects)
AddBlueprintFor(obj.HitObject);
}
selectedHitObjects.BindTo(Beatmap.SelectedHitObjects);
selectedHitObjects.CollectionChanged += (selectedObjects, args) =>
{
@ -69,7 +62,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (Composer != null)
{
// For pooled rulesets, blueprints must be added for hitobjects already "current" as they would've not been "current" during the async load addition process above.
foreach (var obj in Composer.HitObjects)
AddBlueprintFor(obj.HitObject);

View File

@ -48,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
AllowPause = false,
AllowRestart = false,
AllowSkippingIntro = false,
AllowSkipping = false,
})
{
this.userIds = userIds;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
using osu.Game.Storyboards;
@ -19,6 +20,14 @@ namespace osu.Game.Screens.Play
private readonly Storyboard storyboard;
private DrawableStoryboard drawableStoryboard;
/// <summary>
/// Whether the storyboard is considered finished.
/// </summary>
/// <remarks>
/// This is true by default in here, until an actual drawable storyboard is loaded, in which case it'll bind to it.
/// </remarks>
public IBindable<bool> HasStoryboardEnded = new BindableBool(true);
public DimmableStoryboard(Storyboard storyboard)
{
this.storyboard = storyboard;
@ -49,6 +58,7 @@ namespace osu.Game.Screens.Play
return;
drawableStoryboard = storyboard.CreateDrawable();
HasStoryboardEnded.BindTo(drawableStoryboard.HasStoryboardEnded);
if (async)
LoadComponentAsync(drawableStoryboard, onStoryboardCreated);

View File

@ -104,7 +104,8 @@ namespace osu.Game.Screens.Play
private BreakTracker breakTracker;
private SkipOverlay skipOverlay;
private SkipOverlay skipIntroOverlay;
private SkipOverlay skipOutroOverlay;
protected ScoreProcessor ScoreProcessor { get; private set; }
@ -244,7 +245,6 @@ namespace osu.Game.Screens.Play
HUDOverlay.ShowHud.Value = false;
HUDOverlay.ShowHud.Disabled = true;
BreakOverlay.Hide();
skipOverlay.Hide();
}
DrawableRuleset.FrameStableClock.WaitingOnFrames.BindValueChanged(waiting =>
@ -281,8 +281,14 @@ namespace osu.Game.Screens.Play
ScoreProcessor.RevertResult(r);
};
DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded =>
{
if (storyboardEnded.NewValue && completionProgressDelegate == null)
updateCompletionState();
};
// Bind the judgement processors to ourselves
ScoreProcessor.HasCompleted.ValueChanged += updateCompletionState;
ScoreProcessor.HasCompleted.BindValueChanged(_ => updateCompletionState());
HealthProcessor.Failed += onFail;
foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>())
@ -355,10 +361,15 @@ namespace osu.Game.Screens.Play
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
skipOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime)
skipIntroOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime)
{
RequestSkip = performUserRequestedSkip
},
skipOutroOverlay = new SkipOverlay(Beatmap.Value.Storyboard.LatestEventTime ?? 0)
{
RequestSkip = () => updateCompletionState(true),
Alpha = 0
},
FailOverlay = new FailOverlay
{
OnRetry = Restart,
@ -385,12 +396,15 @@ namespace osu.Game.Screens.Play
}
};
if (!Configuration.AllowSkipping || !DrawableRuleset.AllowGameplayOverlays)
{
skipIntroOverlay.Expire();
skipOutroOverlay.Expire();
}
if (GameplayClockContainer is MasterGameplayClockContainer master)
HUDOverlay.PlayerSettingsOverlay.PlaybackSettings.UserPlaybackRate.BindTarget = master.UserPlaybackRate;
if (!Configuration.AllowSkippingIntro)
skipOverlay.Expire();
if (Configuration.AllowRestart)
{
container.Add(new HotkeyRetryOverlay
@ -525,6 +539,10 @@ namespace osu.Game.Screens.Play
Pause();
return;
}
// if the score is ready for display but results screen has not been pushed yet (e.g. storyboard is still playing beyond gameplay), then transition to results screen instead of exiting.
if (prepareScoreForDisplayTask != null)
updateCompletionState(true);
}
this.Exit();
@ -564,17 +582,23 @@ namespace osu.Game.Screens.Play
private ScheduledDelegate completionProgressDelegate;
private Task<ScoreInfo> prepareScoreForDisplayTask;
private void updateCompletionState(ValueChangedEvent<bool> completionState)
/// <summary>
/// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime.
/// </summary>
/// <param name="skipStoryboardOutro">If in a state where a storyboard outro is to be played, offers the choice of skipping beyond it.</param>
/// <exception cref="InvalidOperationException">Thrown if this method is called more than once without changing state.</exception>
private void updateCompletionState(bool skipStoryboardOutro = false)
{
// screen may be in the exiting transition phase.
if (!this.IsCurrentScreen())
return;
if (!completionState.NewValue)
if (!ScoreProcessor.HasCompleted.Value)
{
completionProgressDelegate?.Cancel();
completionProgressDelegate = null;
ValidForResume = true;
skipOutroOverlay.Hide();
return;
}
@ -614,6 +638,20 @@ namespace osu.Game.Screens.Play
return score.ScoreInfo;
});
if (skipStoryboardOutro)
{
scheduleCompletion();
return;
}
bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
if (storyboardHasOutro)
{
skipOutroOverlay.Show();
return;
}
using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY))
scheduleCompletion();
}

View File

@ -21,8 +21,8 @@ namespace osu.Game.Screens.Play
public bool AllowRestart { get; set; } = true;
/// <summary>
/// Whether the player should be allowed to skip the intro, advancing to the start of gameplay.
/// Whether the player should be allowed to skip intros/outros, advancing to the start of gameplay or the end of a storyboard.
/// </summary>
public bool AllowSkippingIntro { get; set; } = true;
public bool AllowSkipping { get; set; } = true;
}
}

View File

@ -8,19 +8,19 @@ 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.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Input.Bindings;
using osu.Game.Screens.Ranking;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Input.Bindings;
namespace osu.Game.Screens.Play
{
@ -92,6 +92,18 @@ namespace osu.Game.Screens.Play
private double fadeOutBeginTime => startTime - MasterGameplayClockContainer.MINIMUM_SKIP_TIME;
public override void Hide()
{
base.Hide();
fadeContainer.Hide();
}
public override void Show()
{
base.Show();
fadeContainer.Show();
}
protected override void LoadComplete()
{
base.LoadComplete();
@ -147,7 +159,7 @@ namespace osu.Game.Screens.Play
{
}
private class FadeContainer : Container, IStateful<Visibility>
public class FadeContainer : Container, IStateful<Visibility>
{
public event Action<Visibility> StateChanged;
@ -170,7 +182,7 @@ namespace osu.Game.Screens.Play
switch (state)
{
case Visibility.Visible:
// we may be triggered to become visible mnultiple times but we only want to transform once.
// we may be triggered to become visible multiple times but we only want to transform once.
if (stateChanged)
this.FadeIn(500, Easing.OutExpo);

View File

@ -11,7 +11,6 @@ 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.Utils;
using osu.Game.Beatmaps;
@ -49,7 +48,9 @@ namespace osu.Game.Screens.Select
private IBindable<StarDifficulty?> beatmapDifficulty;
protected BufferedWedgeInfo Info;
protected Container DisplayedContent { get; private set; }
protected WedgeInfoText Info { get; private set; }
public BeatmapInfoWedge()
{
@ -110,9 +111,9 @@ namespace osu.Game.Screens.Select
}
}
public override bool IsPresent => base.IsPresent || Info == null; // Visibility is updated in the LoadComponentAsync callback
public override bool IsPresent => base.IsPresent || DisplayedContent == null; // Visibility is updated in the LoadComponentAsync callback
private BufferedWedgeInfo loadingInfo;
private Container loadingInfo;
private void updateDisplay()
{
@ -124,9 +125,9 @@ namespace osu.Game.Screens.Select
{
State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible;
Info?.FadeOut(250);
Info?.Expire();
Info = null;
DisplayedContent?.FadeOut(250);
DisplayedContent?.Expire();
DisplayedContent = null;
}
if (beatmap == null)
@ -135,17 +136,23 @@ namespace osu.Game.Screens.Select
return;
}
LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, mods.Value, beatmapDifficulty.Value ?? new StarDifficulty())
LoadComponentAsync(loadingInfo = new Container
{
RelativeSizeAxes = Axes.Both,
Shear = -Shear,
Depth = Info?.Depth + 1 ?? 0
Depth = DisplayedContent?.Depth + 1 ?? 0,
Children = new Drawable[]
{
new BeatmapInfoWedgeBackground(beatmap),
Info = new WedgeInfoText(beatmap, ruleset.Value, mods.Value, beatmapDifficulty.Value ?? new StarDifficulty()),
}
}, loaded =>
{
// ensure we are the most recent loaded wedge.
if (loaded != loadingInfo) return;
removeOldInfo();
Add(Info = loaded);
Add(DisplayedContent = loaded);
});
}
}
@ -156,7 +163,7 @@ namespace osu.Game.Screens.Select
cancellationSource?.Cancel();
}
public class BufferedWedgeInfo : BufferedContainer
public class WedgeInfoText : Container
{
public OsuSpriteText VersionLabel { get; private set; }
public OsuSpriteText TitleLabel { get; private set; }
@ -176,8 +183,7 @@ namespace osu.Game.Screens.Select
private ModSettingChangeTracker settingChangeTracker;
public BufferedWedgeInfo(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList<Mod> mods, StarDifficulty difficulty)
: base(pixelSnapping: true)
public WedgeInfoText(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList<Mod> mods, StarDifficulty difficulty)
{
this.beatmap = beatmap;
ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset;
@ -191,7 +197,6 @@ namespace osu.Game.Screens.Select
var beatmapInfo = beatmap.BeatmapInfo;
var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
CacheDrawnFrameBuffer = true;
RelativeSizeAxes = Axes.Both;
titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.TitleUnicode, metadata.Title));
@ -199,32 +204,6 @@ namespace osu.Game.Screens.Select
Children = new Drawable[]
{
// We will create the white-to-black gradient by modulating transparency and having
// a black backdrop. This results in an sRGB-space gradient and not linear space,
// transitioning from white to black more perceptually uniformly.
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
// We use a container, such that we can set the colour gradient to go across the
// vertices of the masked container instead of the vertices of the (larger) sprite.
new Container
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0.3f)),
Children = new[]
{
// Zoomed-in and cropped beatmap background
new BeatmapBackgroundSprite(beatmap)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fill,
},
},
},
new DifficultyColourBar(starDifficulty)
{
RelativeSizeAxes = Axes.Y,
@ -340,7 +319,6 @@ namespace osu.Game.Screens.Select
{
ArtistLabel.Text = artistBinding.Value;
TitleLabel.Text = string.IsNullOrEmpty(source) ? titleBinding.Value : source + " — " + titleBinding.Value;
ForceRedraw();
}
private void addInfoLabels()
@ -426,8 +404,6 @@ namespace osu.Game.Screens.Select
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm),
Content = labelText
});
ForceRedraw();
}
private OsuSpriteText[] getMapper(BeatmapMetadata metadata)

View File

@ -0,0 +1,66 @@
// 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 osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Framework.Graphics.Shapes;
namespace osu.Game.Screens.Select
{
internal class BeatmapInfoWedgeBackground : CompositeDrawable
{
private readonly WorkingBeatmap beatmap;
public BeatmapInfoWedgeBackground(WorkingBeatmap beatmap)
{
this.beatmap = beatmap;
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.Both;
InternalChild = new BufferedContainer
{
CacheDrawnFrameBuffer = true,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
// We will create the white-to-black gradient by modulating transparency and having
// a black backdrop. This results in an sRGB-space gradient and not linear space,
// transitioning from white to black more perceptually uniformly.
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
// We use a container, such that we can set the colour gradient to go across the
// vertices of the masked container instead of the vertices of the (larger) sprite.
new Container
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0.3f)),
Children = new[]
{
// Zoomed-in and cropped beatmap background
new BeatmapBackgroundSprite(beatmap)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fill,
},
},
},
}
};
}
}
}

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using osuTK;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
@ -19,6 +20,13 @@ namespace osu.Game.Storyboards.Drawables
[Cached]
public Storyboard Storyboard { get; }
/// <summary>
/// Whether the storyboard is considered finished.
/// </summary>
public IBindable<bool> HasStoryboardEnded => hasStoryboardEnded;
private readonly BindableBool hasStoryboardEnded = new BindableBool();
protected override Container<DrawableStoryboardLayer> Content { get; }
protected override Vector2 DrawScale => new Vector2(Parent.DrawHeight / 480);
@ -39,6 +47,8 @@ namespace osu.Game.Storyboards.Drawables
public override bool RemoveCompletedTransforms => false;
private double? lastEventEndTime;
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
@ -73,6 +83,14 @@ namespace osu.Game.Storyboards.Drawables
Add(layer.CreateDrawable());
}
lastEventEndTime = Storyboard.LatestEventTime;
}
protected override void Update()
{
base.Update();
hasStoryboardEnded.Value = lastEventEndTime == null || Time.Current >= lastEventEndTime;
}
public DrawableStoryboardLayer OverlayLayer => Children.Single(layer => layer.Name == "Overlay");

View File

@ -14,4 +14,17 @@ namespace osu.Game.Storyboards
Drawable CreateDrawable();
}
public static class StoryboardElementExtensions
{
/// <summary>
/// Returns the end time of this storyboard element.
/// </summary>
/// <remarks>
/// This returns the <see cref="IStoryboardElementWithDuration.EndTime"/> where available, falling back to <see cref="IStoryboardElement.StartTime"/> otherwise.
/// </remarks>
/// <param name="element">The storyboard element.</param>
/// <returns>The end time of this element.</returns>
public static double GetEndTime(this IStoryboardElement element) => (element as IStoryboardElementWithDuration)?.EndTime ?? element.StartTime;
}
}

View File

@ -0,0 +1,21 @@
// 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.
namespace osu.Game.Storyboards
{
/// <summary>
/// A <see cref="IStoryboardElement"/> that ends at a different time than its start time.
/// </summary>
public interface IStoryboardElementWithDuration : IStoryboardElement
{
/// <summary>
/// The time at which the <see cref="IStoryboardElement"/> ends.
/// </summary>
double EndTime { get; }
/// <summary>
/// The duration of the StoryboardElement.
/// </summary>
double Duration => EndTime - StartTime;
}
}

View File

@ -36,6 +36,16 @@ namespace osu.Game.Storyboards
/// </remarks>
public double? EarliestEventTime => Layers.SelectMany(l => l.Elements).OrderBy(e => e.StartTime).FirstOrDefault()?.StartTime;
/// <summary>
/// Across all layers, find the latest point in time that a storyboard element ends at.
/// Will return null if there are no elements.
/// </summary>
/// <remarks>
/// This iterates all elements and as such should be used sparingly or stored locally.
/// Videos and samples return StartTime as their EndTIme.
/// </remarks>
public double? LatestEventTime => Layers.SelectMany(l => l.Elements).OrderBy(e => e.GetEndTime()).LastOrDefault()?.GetEndTime();
/// <summary>
/// Depth of the currently front-most storyboard layer, excluding the overlay layer.
/// </summary>

View File

@ -11,7 +11,7 @@ using JetBrains.Annotations;
namespace osu.Game.Storyboards
{
public class StoryboardSprite : IStoryboardElement
public class StoryboardSprite : IStoryboardElementWithDuration
{
private readonly List<CommandLoop> loops = new List<CommandLoop>();
private readonly List<CommandTrigger> triggers = new List<CommandTrigger>();