1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 11:07:52 +08:00

Merge branch 'master' into tournament-fix-null-population

This commit is contained in:
Bartłomiej Dach 2023-07-27 22:36:11 +02:00
commit 780b8f0ec8
No known key found for this signature in database
56 changed files with 815 additions and 163 deletions

View File

@ -25,6 +25,35 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
[Test]
public void TestSelectAfterFadedOut()
{
var slider = new Slider
{
StartTime = 0,
Position = new Vector2(100, 100),
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(100))
}
}
};
AddStep("add slider", () => EditorBeatmap.Add(slider));
moveMouseToObject(() => slider);
AddStep("seek after end", () => EditorClock.Seek(750));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("slider not selected", () => EditorBeatmap.SelectedHitObjects.Count == 0);
AddStep("seek to visible", () => EditorClock.Seek(650));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddUntilStep("slider selected", () => EditorBeatmap.SelectedHitObjects.Single() == slider);
}
[Test] [Test]
public void TestContextMenuShownCorrectlyForSelectedSlider() public void TestContextMenuShownCorrectlyForSelectedSlider()
{ {

View File

@ -22,7 +22,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
protected override bool AlwaysShowWhenSelected => true; protected override bool AlwaysShowWhenSelected => true;
protected override bool ShouldBeAlive => base.ShouldBeAlive protected override bool ShouldBeAlive => base.ShouldBeAlive
|| (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION); || (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime
&& editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION);
public override bool IsSelectable =>
// Bypass fade out extension from hit markers for selection purposes.
// This is to match stable, where even when the afterimage hit markers are still visible, objects are not selectable.
base.ShouldBeAlive;
protected OsuSelectionBlueprint(T hitObject) protected OsuSelectionBlueprint(T hitObject)
: base(hitObject) : base(hitObject)

View File

@ -36,6 +36,28 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
AssertResult<Hit>(0, HitResult.Great); AssertResult<Hit>(0, HitResult.Great);
} }
[Test]
public void TestHitWithBothKeysOnSameFrameDoesNotFallThroughToNextObject()
{
PerformTest(new List<ReplayFrame>
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(1000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
}, CreateBeatmap(new Hit
{
Type = HitType.Centre,
StartTime = 1000,
}, new Hit
{
Type = HitType.Centre,
StartTime = 1020
}));
AssertJudgementCount(2);
AssertResult<Hit>(0, HitResult.Great);
AssertResult<Hit>(1, HitResult.Miss);
}
[Test] [Test]
public void TestHitRimHit() public void TestHitRimHit()
{ {

View File

@ -1,24 +1,71 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Tests.Mods namespace osu.Game.Rulesets.Taiko.Tests.Mods
{ {
public partial class TestSceneTaikoModHidden : TaikoModTestScene public partial class TestSceneTaikoModHidden : TaikoModTestScene
{ {
private Func<bool> checkAllMaxResultJudgements(int count) => ()
=> Player.ScoreProcessor.JudgedHits >= count
&& Player.Results.All(result => result.Type == result.Judgement.MaxResult);
[Test] [Test]
public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData
{ {
Mod = new TaikoModHidden(), Mod = new TaikoModHidden(),
Autoplay = true, Autoplay = true,
PassCondition = checkSomeAutoplayHits PassCondition = checkAllMaxResultJudgements(4),
}); });
private bool checkSomeAutoplayHits() [Test]
=> Player.ScoreProcessor.JudgedHits >= 4 public void TestHitTwoNotesWithinShortPeriod()
&& Player.Results.All(result => result.Type == result.Judgement.MaxResult); {
const double hit_time = 1;
var beatmap = new Beatmap<TaikoHitObject>
{
HitObjects = new List<TaikoHitObject>
{
new Hit
{
Type = HitType.Rim,
StartTime = hit_time,
},
new Hit
{
Type = HitType.Centre,
StartTime = hit_time * 2,
},
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty
{
SliderTickRate = 4,
OverallDifficulty = 0,
},
Ruleset = new TaikoRuleset().RulesetInfo
},
};
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
CreateModTest(new ModTestData
{
Mod = new TaikoModHidden(),
Autoplay = true,
PassCondition = checkAllMaxResultJudgements(2),
Beatmap = beatmap,
});
}
} }
} }

View File

@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private bool validActionPressed; private bool validActionPressed;
private bool pressHandledThisFrame; private double? lastPressHandleTime;
private readonly Bindable<HitType> type = new Bindable<HitType>(); private readonly Bindable<HitType> type = new Bindable<HitType>();
@ -76,7 +76,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
HitActions = null; HitActions = null;
HitAction = null; HitAction = null;
validActionPressed = pressHandledThisFrame = false; validActionPressed = false;
lastPressHandleTime = null;
} }
private void updateActionsFromType() private void updateActionsFromType()
@ -114,7 +115,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
{ {
if (pressHandledThisFrame) if (lastPressHandleTime == Time.Current)
return true; return true;
if (Judged) if (Judged)
return false; return false;
@ -128,7 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
// Regardless of whether we've hit or not, any secondary key presses in the same frame should be discarded // Regardless of whether we've hit or not, any secondary key presses in the same frame should be discarded
// E.g. hitting a non-strong centre as a strong should not fall through and perform a hit on the next note // E.g. hitting a non-strong centre as a strong should not fall through and perform a hit on the next note
pressHandledThisFrame = true; lastPressHandleTime = Time.Current;
return result; return result;
} }
@ -139,15 +140,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
base.OnReleased(e); base.OnReleased(e);
} }
protected override void Update()
{
base.Update();
// The input manager processes all input prior to us updating, so this is the perfect time
// for us to remove the extra press blocking, before input is handled in the next frame
pressHandledThisFrame = false;
}
protected override void UpdateHitStateTransforms(ArmedState state) protected override void UpdateHitStateTransforms(ArmedState state)
{ {
Debug.Assert(HitObject.HitWindows != null); Debug.Assert(HitObject.HitWindows != null);

View File

@ -14,14 +14,14 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks namespace osu.Game.Tests.Editing.Checks
{ {
public class CheckDrainTimeTest public class CheckDrainLengthTest
{ {
private CheckDrainTime check = null!; private CheckDrainLength check = null!;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
check = new CheckDrainTime(); check = new CheckDrainLength();
} }
[Test] [Test]
@ -40,7 +40,7 @@ namespace osu.Game.Tests.Editing.Checks
var issues = check.Run(context).ToList(); var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1)); Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckDrainTime.IssueTemplateTooShort); Assert.That(issues.Single().Template is CheckDrainLength.IssueTemplateTooShort);
} }
[Test] [Test]
@ -63,7 +63,7 @@ namespace osu.Game.Tests.Editing.Checks
var issues = check.Run(context).ToList(); var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1)); Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckDrainTime.IssueTemplateTooShort); Assert.That(issues.Single().Template is CheckDrainLength.IssueTemplateTooShort);
} }
[Test] [Test]

View File

@ -340,14 +340,14 @@ namespace osu.Game.Tests.Visual.Editing
public void TestCyclicSelectionBackwards() public void TestCyclicSelectionBackwards()
{ {
var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 }; var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 };
var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 300 }; var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 200 };
var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 }; var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 400 };
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject })); AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject }));
moveMouseToObject(() => firstObject); moveMouseToObject(() => firstObject);
AddStep("seek to third", () => EditorClock.Seek(600)); AddStep("seek to third", () => EditorClock.Seek(350));
AddStep("left click", () => InputManager.Click(MouseButton.Left)); AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject)); AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject));

View File

@ -69,13 +69,13 @@ namespace osu.Game.Tests.Visual.Gameplay
spewer.Clock = new FramedClock(testClock); spewer.Clock = new FramedClock(testClock);
}); });
AddStep("start spewer", () => spewer.Active.Value = true); AddStep("start spewer", () => spewer.Active.Value = true);
AddAssert("spawned first particle", () => spewer.TotalCreatedParticles == 1); AddAssert("spawned first particle", () => spewer.TotalCreatedParticles, () => Is.EqualTo(1));
AddStep("move clock forward", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * 3); AddStep("move clock forward", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * 3);
AddAssert("spawned second particle", () => spewer.TotalCreatedParticles == 2); AddAssert("spawned second particle", () => spewer.TotalCreatedParticles, () => Is.EqualTo(2));
AddStep("move clock backwards", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * -1); AddStep("move clock backwards", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * -1);
AddAssert("spawned third particle", () => spewer.TotalCreatedParticles == 3); AddAssert("spawned third particle", () => spewer.TotalCreatedParticles, () => Is.EqualTo(3));
} }
private TestParticleSpewer createSpewer() => private TestParticleSpewer createSpewer() =>

View File

@ -21,9 +21,11 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
@ -147,6 +149,38 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null)); AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
} }
[Test]
public void TestReplayExport()
{
CreateTest();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
AddUntilStep("results displayed", () => (Player.GetChildScreen() as ResultsScreen)?.IsLoaded == true);
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
AddUntilStep("wait for button clickable", () => ((OsuScreen)Player.GetChildScreen())
.ChildrenOfType<ReplayDownloadButton>().FirstOrDefault()?
.ChildrenOfType<OsuClickableContainer>().FirstOrDefault()?
.Enabled.Value == true);
AddAssert("no export files", () => !LocalStorage.GetFiles("exports").Any());
AddStep("Export replay", () => InputManager.PressKey(Key.F2));
string? filePath = null;
// Files starting with _ are temporary, created by CreateFileSafely call.
AddUntilStep("wait for export file", () => filePath = LocalStorage.GetFiles("exports").SingleOrDefault(f => !f.StartsWith("_", StringComparison.Ordinal)), () => Is.Not.Null);
AddAssert("filesize is non-zero", () =>
{
using (var stream = LocalStorage.GetStream(filePath))
return stream.Length;
}, () => Is.Not.Zero);
}
[Test] [Test]
public void TestScoreStoredLocallyCustomRuleset() public void TestScoreStoredLocallyCustomRuleset()
{ {

View File

@ -20,7 +20,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
CreateModTest(new ModTestData CreateModTest(new ModTestData
{ {
Beatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo).Beatmap,
Mod = new UnknownMod("WNG"), Mod = new UnknownMod("WNG"),
PassCondition = () => Player.IsLoaded && !Player.LoadedBeatmapSuccessfully PassCondition = () => Player.IsLoaded && !Player.LoadedBeatmapSuccessfully
}); });

View File

@ -0,0 +1,52 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Screens.Menu;
namespace osu.Game.Tests.Visual.Menus
{
[TestFixture]
public partial class TestSceneStarFountain : OsuTestScene
{
[SetUpSteps]
public void SetUpSteps()
{
AddStep("make fountains", () =>
{
Children = new[]
{
new StarFountain
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
X = 200,
},
new StarFountain
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
X = -200,
},
};
});
}
[Test]
public void TestPew()
{
AddRepeatStep("activate fountains sometimes", () =>
{
foreach (var fountain in Children.OfType<StarFountain>())
{
if (RNG.NextSingle() > 0.8f)
fountain.Shoot();
}
}, 150);
}
}
}

View File

@ -5,7 +5,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
@ -283,8 +282,6 @@ namespace osu.Game.Tests.Visual.UserInterface
if (ReferenceEquals(timingPoints[^1], current)) if (ReferenceEquals(timingPoints[^1], current))
{ {
Debug.Assert(BeatSyncSource.Clock != null);
return (int)Math.Ceiling((BeatSyncSource.Clock.CurrentTime - current.Time) / current.BeatLength); return (int)Math.Ceiling((BeatSyncSource.Clock.CurrentTime - current.Time) / current.BeatLength);
} }
@ -295,8 +292,6 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
base.Update(); base.Update();
Debug.Assert(BeatSyncSource.Clock != null);
timeUntilNextBeat.Value = TimeUntilNextBeat; timeUntilNextBeat.Value = TimeUntilNextBeat;
timeSinceLastBeat.Value = TimeSinceLastBeat; timeSinceLastBeat.Value = TimeSinceLastBeat;
currentTime.Value = BeatSyncSource.Clock.CurrentTime; currentTime.Value = BeatSyncSource.Clock.CurrentTime;

View File

@ -542,7 +542,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("clear search", () => modSelectOverlay.SearchTerm = string.Empty); AddStep("clear search", () => modSelectOverlay.SearchTerm = string.Empty);
AddStep("press enter", () => InputManager.Key(Key.Enter)); AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("mod select still visible", () => modSelectOverlay.State.Value == Visibility.Visible); AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
} }
[Test] [Test]

View File

@ -1,23 +1,100 @@
// 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 osu.Framework.Allocation; using System;
using System.Linq;
using Newtonsoft.Json;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Tournament.Screens.Editors; using osu.Game.Tournament.Screens.Editors;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Dialog;
using osu.Game.Tournament.Screens.Editors.Components;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tournament.Tests.Screens namespace osu.Game.Tournament.Tests.Screens
{ {
public partial class TestSceneLadderEditorScreen : TournamentScreenTestScene public partial class TestSceneLadderEditorScreen : TournamentScreenTestScene
{ {
[BackgroundDependencyLoader] private LadderEditorScreen ladderEditorScreen = null!;
private void load() private OsuContextMenuContainer? osuContextMenuContainer;
[SetUp]
public void Setup() => Schedule(() =>
{ {
Add(new OsuContextMenuContainer ladderEditorScreen = new LadderEditorScreen();
Add(osuContextMenuContainer = new OsuContextMenuContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = new LadderEditorScreen() Child = ladderEditorScreen = new LadderEditorScreen()
}); });
});
[Test]
public void TestResetBracketTeamsCancelled()
{
Bindable<string> matchBeforeReset = new Bindable<string>();
AddStep("save current match state", () =>
{
matchBeforeReset.Value = JsonConvert.SerializeObject(Ladder.CurrentMatch.Value);
});
AddStep("pull up context menu", () =>
{
InputManager.MoveMouseTo(ladderEditorScreen);
InputManager.Click(MouseButton.Right);
});
AddStep("click Reset teams button", () =>
{
InputManager.MoveMouseTo(osuContextMenuContainer.ChildrenOfType<DrawableOsuMenuItem>().Last(p =>
((OsuMenuItem)p.Item).Type == MenuItemType.Destructive), new Vector2(5, 0));
InputManager.Click(MouseButton.Left);
});
AddAssert("dialog displayed", () => DialogOverlay.CurrentDialog is LadderResetTeamsDialog);
AddStep("click cancel", () =>
{
InputManager.MoveMouseTo(DialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogButton>().Last());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("dialog dismissed", () => DialogOverlay.CurrentDialog is not LadderResetTeamsDialog);
AddAssert("assert ladder teams unchanged", () => string.Equals(matchBeforeReset.Value, JsonConvert.SerializeObject(Ladder.CurrentMatch.Value), StringComparison.Ordinal));
}
[Test]
public void TestResetBracketTeams()
{
AddStep("pull up context menu", () =>
{
InputManager.MoveMouseTo(ladderEditorScreen);
InputManager.Click(MouseButton.Right);
});
AddStep("click Reset teams button", () =>
{
InputManager.MoveMouseTo(osuContextMenuContainer.ChildrenOfType<DrawableOsuMenuItem>().Last(p =>
((OsuMenuItem)p.Item).Type == MenuItemType.Destructive), new Vector2(5, 0));
InputManager.Click(MouseButton.Left);
});
AddAssert("dialog displayed", () => DialogOverlay.CurrentDialog is LadderResetTeamsDialog);
AddStep("click confirmation", () =>
{
InputManager.MoveMouseTo(DialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogButton>().First());
InputManager.PressButton(MouseButton.Left);
});
AddUntilStep("dialog dismissed", () => DialogOverlay.CurrentDialog is not LadderResetTeamsDialog);
AddStep("release mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("assert ladder teams reset", () => Ladder.CurrentMatch.Value.Team1.Value == null && Ladder.CurrentMatch.Value.Team2.Value == null);
} }
} }
} }

View File

@ -1,13 +1,15 @@
// 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 osu.Framework.Allocation;
using osu.Game.Tournament.Screens.Editors; using osu.Game.Tournament.Screens.Editors;
namespace osu.Game.Tournament.Tests.Screens namespace osu.Game.Tournament.Tests.Screens
{ {
public partial class TestSceneRoundEditorScreen : TournamentScreenTestScene public partial class TestSceneRoundEditorScreen : TournamentScreenTestScene
{ {
public TestSceneRoundEditorScreen() [BackgroundDependencyLoader]
private void load()
{ {
Add(new RoundEditorScreen Add(new RoundEditorScreen
{ {

View File

@ -12,7 +12,8 @@ namespace osu.Game.Tournament.Tests.Screens
[Cached] [Cached]
private readonly LadderInfo ladder = new LadderInfo(); private readonly LadderInfo ladder = new LadderInfo();
public TestSceneSeedingEditorScreen() [BackgroundDependencyLoader]
private void load()
{ {
var match = CreateSampleMatch(); var match = CreateSampleMatch();

View File

@ -1,13 +1,15 @@
// 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 osu.Framework.Allocation;
using osu.Game.Tournament.Screens.Editors; using osu.Game.Tournament.Screens.Editors;
namespace osu.Game.Tournament.Tests.Screens namespace osu.Game.Tournament.Tests.Screens
{ {
public partial class TestSceneTeamEditorScreen : TournamentScreenTestScene public partial class TestSceneTeamEditorScreen : TournamentScreenTestScene
{ {
public TestSceneTeamEditorScreen() [BackgroundDependencyLoader]
private void load()
{ {
Add(new TeamEditorScreen Add(new TeamEditorScreen
{ {

View File

@ -8,6 +8,7 @@ using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osu.Game.Tournament.IO; using osu.Game.Tournament.IO;
@ -16,8 +17,11 @@ using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Tests namespace osu.Game.Tournament.Tests
{ {
public abstract partial class TournamentTestScene : OsuTestScene public abstract partial class TournamentTestScene : OsuManualInputManagerTestScene
{ {
[Cached(typeof(IDialogOverlay))]
protected readonly DialogOverlay DialogOverlay = new DialogOverlay { Depth = float.MinValue };
[Cached] [Cached]
protected LadderInfo Ladder { get; private set; } = new LadderInfo(); protected LadderInfo Ladder { get; private set; } = new LadderInfo();
@ -43,6 +47,8 @@ namespace osu.Game.Tournament.Tests
Ruleset.BindTo(Ladder.Ruleset); Ruleset.BindTo(Ladder.Ruleset);
Dependencies.CacheAs(new StableInfo(storage)); Dependencies.CacheAs(new StableInfo(storage));
Add(DialogOverlay);
} }
[SetUpSteps] [SetUpSteps]

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays.Dialog;
using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Screens.Editors.Components
{
public partial class DeleteRoundDialog : DangerousActionDialog
{
public DeleteRoundDialog(TournamentRound round, Action action)
{
HeaderText = round.Name.Value.Length > 0 ? $@"Delete round ""{round.Name.Value}""?" : @"Delete unnamed round?";
Icon = FontAwesome.Solid.Trash;
DangerousAction = action;
}
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays.Dialog;
using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Screens.Editors.Components
{
public partial class DeleteTeamDialog : DangerousActionDialog
{
public DeleteTeamDialog(TournamentTeam team, Action action)
{
HeaderText = team.FullName.Value.Length > 0 ? $@"Delete team ""{team.FullName.Value}""?" :
team.Acronym.Value.Length > 0 ? $@"Delete team ""{team.Acronym.Value}""?" :
@"Delete unnamed team?";
Icon = FontAwesome.Solid.Trash;
DangerousAction = action;
}
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Tournament.Screens.Editors.Components
{
public partial class LadderResetTeamsDialog : DangerousActionDialog
{
public LadderResetTeamsDialog(Action action)
{
HeaderText = @"Reset teams?";
Icon = FontAwesome.Solid.Undo;
DangerousAction = action;
}
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Tournament.Screens.Editors.Components
{
public partial class TournamentClearAllDialog : DangerousActionDialog
{
public TournamentClearAllDialog(Action action)
{
HeaderText = @"Clear all?";
Icon = FontAwesome.Solid.Trash;
DangerousAction = action;
}
}
}

View File

@ -6,6 +6,7 @@
using System; using System;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -16,7 +17,9 @@ using osu.Framework.Input.States;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tournament.Components; using osu.Game.Tournament.Components;
using osu.Game.Overlays;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
using osu.Game.Tournament.Screens.Editors.Components;
using osu.Game.Tournament.Screens.Ladder; using osu.Game.Tournament.Screens.Ladder;
using osu.Game.Tournament.Screens.Ladder.Components; using osu.Game.Tournament.Screens.Ladder.Components;
using osuTK; using osuTK;
@ -34,6 +37,10 @@ namespace osu.Game.Tournament.Screens.Editors
private WarningBox rightClickMessage; private WarningBox rightClickMessage;
[Resolved(canBeNull: true)]
[CanBeNull]
private IDialogOverlay dialogOverlay { get; set; }
protected override bool DrawLoserPaths => true; protected override bool DrawLoserPaths => true;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -87,8 +94,11 @@ namespace osu.Game.Tournament.Screens.Editors
}), }),
new OsuMenuItem("Reset teams", MenuItemType.Destructive, () => new OsuMenuItem("Reset teams", MenuItemType.Destructive, () =>
{ {
foreach (var p in MatchesContainer) dialogOverlay?.Push(new LadderResetTeamsDialog(() =>
p.Match.Reset(); {
foreach (var p in MatchesContainer)
p.Match.Reset();
}));
}) })
}; };
} }

View File

@ -11,9 +11,11 @@ using osu.Game.Graphics;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Tournament.Components; using osu.Game.Tournament.Components;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
using osu.Game.Tournament.Screens.Editors.Components;
using osuTK; using osuTK;
namespace osu.Game.Tournament.Screens.Editors namespace osu.Game.Tournament.Screens.Editors
@ -29,6 +31,9 @@ namespace osu.Game.Tournament.Screens.Editors
[Resolved] [Resolved]
private LadderInfo ladderInfo { get; set; } = null!; private LadderInfo ladderInfo { get; set; } = null!;
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
public RoundRow(TournamentRound round) public RoundRow(TournamentRound round)
{ {
Model = round; Model = round;
@ -99,11 +104,11 @@ namespace osu.Game.Tournament.Screens.Editors
RelativeSizeAxes = Axes.None, RelativeSizeAxes = Axes.None,
Width = 150, Width = 150,
Text = "Delete Round", Text = "Delete Round",
Action = () => Action = () => dialogOverlay?.Push(new DeleteRoundDialog(Model, () =>
{ {
Expire(); Expire();
ladderInfo.Rounds.Remove(Model); ladderInfo.Rounds.Remove(Model);
}, }))
} }
}; };

View File

@ -11,8 +11,10 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
using osu.Game.Tournament.Screens.Editors.Components;
using osu.Game.Tournament.Screens.Drawings.Components; using osu.Game.Tournament.Screens.Drawings.Components;
using osu.Game.Users; using osu.Game.Users;
using osuTK; using osuTK;
@ -61,6 +63,9 @@ namespace osu.Game.Tournament.Screens.Editors
[Resolved] [Resolved]
private TournamentSceneManager? sceneManager { get; set; } private TournamentSceneManager? sceneManager { get; set; }
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
[Resolved] [Resolved]
private LadderInfo ladderInfo { get; set; } = null!; private LadderInfo ladderInfo { get; set; } = null!;
@ -157,11 +162,11 @@ namespace osu.Game.Tournament.Screens.Editors
Text = "Delete Team", Text = "Delete Team",
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
Action = () => Action = () => dialogOverlay?.Push(new DeleteTeamDialog(Model, () =>
{ {
Expire(); Expire();
ladderInfo.Teams.Remove(Model); ladderInfo.Teams.Remove(Model);
}, })),
}, },
} }
}, },

View File

@ -13,7 +13,9 @@ using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Tournament.Components; using osu.Game.Tournament.Components;
using osu.Game.Tournament.Screens.Editors.Components;
using osuTK; using osuTK;
namespace osu.Game.Tournament.Screens.Editors namespace osu.Game.Tournament.Screens.Editors
@ -24,6 +26,9 @@ namespace osu.Game.Tournament.Screens.Editors
{ {
protected abstract BindableList<TModel> Storage { get; } protected abstract BindableList<TModel> Storage { get; }
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
private FillFlowContainer<TDrawable> flow = null!; private FillFlowContainer<TDrawable> flow = null!;
[Resolved] [Resolved]
@ -79,7 +84,10 @@ namespace osu.Game.Tournament.Screens.Editors
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
BackgroundColour = colours.Pink3, BackgroundColour = colours.Pink3,
Text = "Clear all", Text = "Clear all",
Action = Storage.Clear Action = () =>
{
dialogOverlay?.Push(new TournamentClearAllDialog(() => Storage.Clear()));
}
}, },
} }
} }

View File

@ -139,7 +139,7 @@ namespace osu.Game.Tournament.Screens.Gameplay
base.LoadComplete(); base.LoadComplete();
State.BindTo(ipc.State); State.BindTo(ipc.State);
State.BindValueChanged(stateChanged, true); State.BindValueChanged(_ => updateState(), true);
} }
protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match) protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
@ -150,20 +150,47 @@ namespace osu.Game.Tournament.Screens.Gameplay
return; return;
warmup.Value = match.NewValue.Team1Score.Value + match.NewValue.Team2Score.Value == 0; warmup.Value = match.NewValue.Team1Score.Value + match.NewValue.Team2Score.Value == 0;
scheduledOperation?.Cancel(); scheduledScreenChange?.Cancel();
} }
private ScheduledDelegate scheduledOperation; private ScheduledDelegate scheduledScreenChange;
private ScheduledDelegate scheduledContract;
private TournamentMatchScoreDisplay scoreDisplay; private TournamentMatchScoreDisplay scoreDisplay;
private TourneyState lastState; private TourneyState lastState;
private MatchHeader header; private MatchHeader header;
private void stateChanged(ValueChangedEvent<TourneyState> state) private void contract()
{
scheduledContract?.Cancel();
SongBar.Expanded = false;
scoreDisplay.FadeOut(100);
using (chat?.BeginDelayedSequence(500))
chat?.Expand();
}
private void expand()
{
scheduledContract?.Cancel();
chat?.Contract();
using (BeginDelayedSequence(300))
{
scoreDisplay.FadeIn(100);
SongBar.Expanded = true;
}
}
private void updateState()
{ {
try try
{ {
if (state.NewValue == TourneyState.Ranking) scheduledScreenChange?.Cancel();
if (State.Value == TourneyState.Ranking)
{ {
if (warmup.Value || CurrentMatch.Value == null) return; if (warmup.Value || CurrentMatch.Value == null) return;
@ -173,28 +200,7 @@ namespace osu.Game.Tournament.Screens.Gameplay
CurrentMatch.Value.Team2Score.Value++; CurrentMatch.Value.Team2Score.Value++;
} }
scheduledOperation?.Cancel(); switch (State.Value)
void expand()
{
chat?.Contract();
using (BeginDelayedSequence(300))
{
scoreDisplay.FadeIn(100);
SongBar.Expanded = true;
}
}
void contract()
{
SongBar.Expanded = false;
scoreDisplay.FadeOut(100);
using (chat?.BeginDelayedSequence(500))
chat?.Expand();
}
switch (state.NewValue)
{ {
case TourneyState.Idle: case TourneyState.Idle:
contract(); contract();
@ -208,30 +214,41 @@ namespace osu.Game.Tournament.Screens.Gameplay
if (lastState == TourneyState.Ranking && !warmup.Value) if (lastState == TourneyState.Ranking && !warmup.Value)
{ {
if (CurrentMatch.Value?.Completed.Value == true) if (CurrentMatch.Value?.Completed.Value == true)
scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(TeamWinScreen)); }, delay_before_progression); scheduledScreenChange = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(TeamWinScreen)); }, delay_before_progression);
else if (CurrentMatch.Value?.Completed.Value == false) else if (CurrentMatch.Value?.Completed.Value == false)
scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(MapPoolScreen)); }, delay_before_progression); scheduledScreenChange = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(MapPoolScreen)); }, delay_before_progression);
} }
} }
break; break;
case TourneyState.Ranking: case TourneyState.Ranking:
scheduledOperation = Scheduler.AddDelayed(contract, 10000); scheduledContract = Scheduler.AddDelayed(contract, 10000);
break; break;
default: default:
chat.Contract();
expand(); expand();
break; break;
} }
} }
finally finally
{ {
lastState = state.NewValue; lastState = State.Value;
} }
} }
public override void Hide()
{
scheduledScreenChange?.Cancel();
base.Hide();
}
public override void Show()
{
updateState();
base.Show();
}
private partial class ChromaArea : CompositeDrawable private partial class ChromaArea : CompositeDrawable
{ {
[Resolved] [Resolved]

View File

@ -26,7 +26,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
private readonly bool editor; private readonly bool editor;
protected readonly FillFlowContainer<DrawableMatchTeam> Flow; protected readonly FillFlowContainer<DrawableMatchTeam> Flow;
private readonly Drawable selectionBox; private readonly Drawable selectionBox;
protected readonly Drawable CurrentMatchSelectionBox; private readonly Drawable currentMatchSelectionBox;
private Bindable<TournamentMatch> globalSelection; private Bindable<TournamentMatch> globalSelection;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
@ -82,7 +82,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
Padding = new MarginPadding(-(spacing + border_thickness)), Padding = new MarginPadding(-(spacing + border_thickness)),
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Child = CurrentMatchSelectionBox = new Container Child = currentMatchSelectionBox = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Alpha = 0, Alpha = 0,
@ -151,9 +151,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
private void updateCurrentMatch() private void updateCurrentMatch()
{ {
if (Match.Current.Value) if (Match.Current.Value)
CurrentMatchSelectionBox.Show(); currentMatchSelectionBox.Show();
else else
CurrentMatchSelectionBox.Hide(); currentMatchSelectionBox.Hide();
} }
private bool selected; private bool selected;

View File

@ -37,6 +37,8 @@ namespace osu.Game.Tournament.Screens.MapPool
private OsuButton buttonRedPick; private OsuButton buttonRedPick;
private OsuButton buttonBluePick; private OsuButton buttonBluePick;
private ScheduledDelegate scheduledScreenChange;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(MatchIPCInfo ipc) private void load(MatchIPCInfo ipc)
{ {
@ -188,8 +190,6 @@ namespace osu.Game.Tournament.Screens.MapPool
setNextMode(); setNextMode();
} }
private ScheduledDelegate scheduledChange;
private void addForBeatmap(int beatmapId) private void addForBeatmap(int beatmapId)
{ {
if (CurrentMatch.Value == null) if (CurrentMatch.Value == null)
@ -216,12 +216,18 @@ namespace osu.Game.Tournament.Screens.MapPool
{ {
if (pickType == ChoiceType.Pick && CurrentMatch.Value.PicksBans.Any(i => i.Type == ChoiceType.Pick)) if (pickType == ChoiceType.Pick && CurrentMatch.Value.PicksBans.Any(i => i.Type == ChoiceType.Pick))
{ {
scheduledChange?.Cancel(); scheduledScreenChange?.Cancel();
scheduledChange = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(GameplayScreen)); }, 10000); scheduledScreenChange = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(GameplayScreen)); }, 10000);
} }
} }
} }
public override void Hide()
{
scheduledScreenChange?.Cancel();
base.Hide();
}
protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match) protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
{ {
base.CurrentMatchChanged(match); base.CurrentMatchChanged(match);

View File

@ -218,8 +218,6 @@ namespace osu.Game.Tournament.Screens.Schedule
Scale = new Vector2(0.8f); Scale = new Vector2(0.8f);
CurrentMatchSelectionBox.Scale = new Vector2(1.02f, 1.15f);
bool conditional = match is ConditionalTournamentMatch; bool conditional = match is ConditionalTournamentMatch;
if (conditional) if (conditional)

View File

@ -17,6 +17,7 @@ using osu.Framework.Platform;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
using osuTK.Graphics; using osuTK.Graphics;
@ -41,6 +42,9 @@ namespace osu.Game.Tournament
private LoadingSpinner loadingSpinner; private LoadingSpinner loadingSpinner;
[Cached(typeof(IDialogOverlay))]
private readonly DialogOverlay dialogOverlay = new DialogOverlay();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(FrameworkConfigManager frameworkConfig, GameHost host) private void load(FrameworkConfigManager frameworkConfig, GameHost host)
{ {
@ -95,12 +99,12 @@ namespace osu.Game.Tournament
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = new TournamentSceneManager() Child = new TournamentSceneManager()
} },
dialogOverlay
}, drawables => }, drawables =>
{ {
loadingSpinner.Hide(); loadingSpinner.Hide();
loadingSpinner.Expire(); loadingSpinner.Expire();
AddRange(drawables); AddRange(drawables);
windowSize.BindValueChanged(size => ScheduleAfterChildren(() => windowSize.BindValueChanged(size => ScheduleAfterChildren(() =>

View File

@ -5,14 +5,9 @@ namespace osu.Game.Beatmaps
{ {
public static class BeatSyncProviderExtensions public static class BeatSyncProviderExtensions
{ {
/// <summary>
/// Check whether beat sync is currently available.
/// </summary>
public static bool CheckBeatSyncAvailable(this IBeatSyncProvider provider) => provider.Clock != null;
/// <summary> /// <summary>
/// Whether the beat sync provider is currently in a kiai section. Should make everything more epic. /// Whether the beat sync provider is currently in a kiai section. Should make everything more epic.
/// </summary> /// </summary>
public static bool CheckIsKiaiTime(this IBeatSyncProvider provider) => provider.Clock != null && provider.ControlPoints?.EffectPointAt(provider.Clock.CurrentTime).KiaiMode == true; public static bool CheckIsKiaiTime(this IBeatSyncProvider provider) => provider.ControlPoints?.EffectPointAt(provider.Clock.CurrentTime).KiaiMode == true;
} }
} }

View File

@ -22,8 +22,8 @@ namespace osu.Game.Beatmaps
ControlPointInfo? ControlPoints { get; } ControlPointInfo? ControlPoints { get; }
/// <summary> /// <summary>
/// Access a clock currently responsible for providing beat sync. If <c>null</c>, no current provider is available. /// Access a clock currently responsible for providing beat sync.
/// </summary> /// </summary>
IClock? Clock { get; } IClock Clock { get; }
} }
} }

View File

@ -110,6 +110,11 @@ namespace osu.Game.Beatmaps
/// </remarks> /// </remarks>
public static double CalculatePlayableLength(this IBeatmap beatmap) => CalculatePlayableLength(beatmap.HitObjects); public static double CalculatePlayableLength(this IBeatmap beatmap) => CalculatePlayableLength(beatmap.HitObjects);
/// <summary>
/// Find the total milliseconds between the first and last hittable objects, excluding any break time.
/// </summary>
public static double CalculateDrainLength(this IBeatmap beatmap) => CalculatePlayableLength(beatmap.HitObjects) - beatmap.TotalBreakTime;
/// <summary> /// <summary>
/// Find the timestamps in milliseconds of the start and end of the playable region. /// Find the timestamps in milliseconds of the start and end of the playable region.
/// </summary> /// </summary>

View File

@ -2,7 +2,6 @@
// 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.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -86,14 +85,12 @@ namespace osu.Game.Graphics.Containers
TimingControlPoint timingPoint; TimingControlPoint timingPoint;
EffectControlPoint effectPoint; EffectControlPoint effectPoint;
IsBeatSyncedWithTrack = BeatSyncSource.CheckBeatSyncAvailable() && BeatSyncSource.Clock?.IsRunning == true; IsBeatSyncedWithTrack = BeatSyncSource.Clock.IsRunning;
double currentTrackTime; double currentTrackTime;
if (IsBeatSyncedWithTrack) if (IsBeatSyncedWithTrack)
{ {
Debug.Assert(BeatSyncSource.Clock != null);
currentTrackTime = BeatSyncSource.Clock.CurrentTime + EarlyActivationMilliseconds; currentTrackTime = BeatSyncSource.Clock.CurrentTime + EarlyActivationMilliseconds;
timingPoint = BeatSyncSource.ControlPoints?.TimingPointAt(currentTrackTime) ?? TimingControlPoint.DEFAULT; timingPoint = BeatSyncSource.ControlPoints?.TimingPointAt(currentTrackTime) ?? TimingControlPoint.DEFAULT;

View File

@ -4,6 +4,7 @@
#nullable disable #nullable disable
using System; using System;
using System.Diagnostics;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -20,9 +21,9 @@ namespace osu.Game.Graphics
{ {
private readonly FallingParticle[] particles; private readonly FallingParticle[] particles;
private int currentIndex; private int currentIndex;
private double lastParticleAdded; private double? lastParticleAdded;
private readonly double cooldown; private readonly double timeBetweenSpawns;
private readonly double maxDuration; private readonly double maxDuration;
/// <summary> /// <summary>
@ -44,7 +45,7 @@ namespace osu.Game.Graphics
particles = new FallingParticle[perSecond * (int)Math.Ceiling(maxDuration / 1000)]; particles = new FallingParticle[perSecond * (int)Math.Ceiling(maxDuration / 1000)];
cooldown = 1000f / perSecond; timeBetweenSpawns = 1000f / perSecond;
this.maxDuration = maxDuration; this.maxDuration = maxDuration;
} }
@ -52,18 +53,52 @@ namespace osu.Game.Graphics
{ {
base.Update(); base.Update();
if (Active.Value && CanSpawnParticles && Math.Abs(Time.Current - lastParticleAdded) > cooldown) Invalidate(Invalidation.DrawNode);
if (!Active.Value || !CanSpawnParticles)
{ {
var newParticle = CreateParticle(); lastParticleAdded = null;
newParticle.StartTime = (float)Time.Current; return;
particles[currentIndex] = newParticle;
currentIndex = (currentIndex + 1) % particles.Length;
lastParticleAdded = Time.Current;
} }
Invalidate(Invalidation.DrawNode); // Always want to spawn the first particle in an activation immediately.
if (lastParticleAdded == null)
{
lastParticleAdded = Time.Current;
spawnParticle();
return;
}
double timeElapsed = Time.Current - lastParticleAdded.Value;
// Avoid spawning too many particles if a long amount of time has passed.
if (Math.Abs(timeElapsed) > maxDuration)
{
lastParticleAdded = Time.Current;
spawnParticle();
return;
}
Debug.Assert(lastParticleAdded != null);
for (int i = 0; i < timeElapsed / timeBetweenSpawns; i++)
{
lastParticleAdded += timeBetweenSpawns;
spawnParticle();
}
}
private void spawnParticle()
{
Debug.Assert(lastParticleAdded != null);
var newParticle = CreateParticle();
newParticle.StartTime = (float)lastParticleAdded.Value;
particles[currentIndex] = newParticle;
currentIndex = (currentIndex + 1) % particles.Length;
} }
/// <summary> /// <summary>
@ -73,7 +108,7 @@ namespace osu.Game.Graphics
protected override DrawNode CreateDrawNode() => new ParticleSpewerDrawNode(this); protected override DrawNode CreateDrawNode() => new ParticleSpewerDrawNode(this);
# region DrawNode #region DrawNode
private class ParticleSpewerDrawNode : SpriteDrawNode private class ParticleSpewerDrawNode : SpriteDrawNode
{ {

View File

@ -950,9 +950,9 @@ namespace osu.Game
if (!args?.Any(a => a == @"--no-version-overlay") ?? true) if (!args?.Any(a => a == @"--no-version-overlay") ?? true)
loadComponentSingleFile(versionManager = new VersionManager { Depth = int.MinValue }, ScreenContainer.Add); loadComponentSingleFile(versionManager = new VersionManager { Depth = int.MinValue }, ScreenContainer.Add);
loadComponentSingleFile(osuLogo, logo => loadComponentSingleFile(osuLogo, _ =>
{ {
logoContainer.Add(logo); osuLogo.SetupDefaultContainer(logoContainer);
// Loader has to be created after the logo has finished loading as Loader performs logo transformations on entering. // Loader has to be created after the logo has finished loading as Loader performs logo transformations on entering.
ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both)); ScreenStack.Push(CreateLoader().With(l => l.RelativeSizeAxes = Axes.Both));

View File

@ -638,8 +638,12 @@ namespace osu.Game.Overlays.Mods
case GlobalAction.Select: case GlobalAction.Select:
{ {
// Pressing select should select first filtered mod if a search is in progress. // Pressing select should select first filtered mod if a search is in progress.
// If there is no search in progress, it should exit the dialog (a bit weird, but this is the expectation from stable).
if (string.IsNullOrEmpty(SearchTerm)) if (string.IsNullOrEmpty(SearchTerm))
{
hideOverlay(true);
return true; return true;
}
ModState? firstMod = columnFlow.Columns.OfType<ModColumn>().FirstOrDefault(m => m.IsPresent)?.AvailableMods.FirstOrDefault(x => x.Visible); ModState? firstMod = columnFlow.Columns.OfType<ModColumn>().FirstOrDefault(m => m.IsPresent)?.AvailableMods.FirstOrDefault(x => x.Visible);

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Edit
new CheckUnsnappedObjects(), new CheckUnsnappedObjects(),
new CheckConcurrentObjects(), new CheckConcurrentObjects(),
new CheckZeroLengthObjects(), new CheckZeroLengthObjects(),
new CheckDrainTime(), new CheckDrainLength(),
// Timing // Timing
new CheckPreviewTime(), new CheckPreviewTime(),

View File

@ -7,10 +7,11 @@ using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks namespace osu.Game.Rulesets.Edit.Checks
{ {
public class CheckDrainTime : ICheck public class CheckDrainLength : ICheck
{ {
private const int min_drain_threshold = 30 * 1000; private const int min_drain_threshold = 30 * 1000;
public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Compose, "Too short drain time");
public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Compose, "Drain length is too short");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[] public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{ {
@ -19,7 +20,7 @@ namespace osu.Game.Rulesets.Edit.Checks
public IEnumerable<Issue> Run(BeatmapVerifierContext context) public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{ {
double drainTime = context.Beatmap.CalculatePlayableLength() - context.Beatmap.TotalBreakTime; double drainTime = context.Beatmap.CalculateDrainLength();
if (drainTime < min_drain_threshold) if (drainTime < min_drain_threshold)
yield return new IssueTemplateTooShort(this).Create((int)(drainTime / 1000)); yield return new IssueTemplateTooShort(this).Create((int)(drainTime / 1000));

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Edit
/// </summary> /// </summary>
public event Action<SelectionBlueprint<T>> Deselected; public event Action<SelectionBlueprint<T>> Deselected;
public override bool HandlePositionalInput => ShouldBeAlive; public override bool HandlePositionalInput => IsSelectable;
public override bool RemoveWhenNotAlive => false; public override bool RemoveWhenNotAlive => false;
protected SelectionBlueprint(T item) protected SelectionBlueprint(T item)
@ -125,6 +125,11 @@ namespace osu.Game.Rulesets.Edit
/// </summary> /// </summary>
public virtual MenuItem[] ContextMenuItems => Array.Empty<MenuItem>(); public virtual MenuItem[] ContextMenuItems => Array.Empty<MenuItem>();
/// <summary>
/// Whether the <see cref="SelectionBlueprint{T}"/> can be currently selected via a click or a drag box.
/// </summary>
public virtual bool IsSelectable => ShouldBeAlive && IsPresent;
/// <summary> /// <summary>
/// The screen-space main point that causes this <see cref="HitObjectSelectionBlueprint"/> to be selected via a drag. /// The screen-space main point that causes this <see cref="HitObjectSelectionBlueprint"/> to be selected via a drag.
/// </summary> /// </summary>

View File

@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mods
int timeSignature = timingPoint.TimeSignature.Numerator; int timeSignature = timingPoint.TimeSignature.Numerator;
// play metronome from one measure before the first object. // play metronome from one measure before the first object.
if (BeatSyncSource.Clock?.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature) if (BeatSyncSource.Clock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature)
return; return;
sample.Frequency.Value = beatIndex % timeSignature == 0 ? 1 : 0.5f; sample.Frequency.Value = beatIndex % timeSignature == 0 ? 1 : 0.5f;

View File

@ -487,7 +487,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
break; break;
case SelectionState.NotSelected: case SelectionState.NotSelected:
if (blueprint.IsAlive && blueprint.IsPresent && quad.Contains(blueprint.ScreenSpaceSelectionPoint)) if (blueprint.IsSelectable && quad.Contains(blueprint.ScreenSpaceSelectionPoint))
blueprint.Select(); blueprint.Select();
break; break;
} }

View File

@ -240,7 +240,7 @@ namespace osu.Game.Screens.Edit.Timing
{ {
base.Update(); base.Update();
if (BeatSyncSource.ControlPoints == null || BeatSyncSource.Clock == null) if (BeatSyncSource.ControlPoints == null)
return; return;
metronomeClock.Rate = IsBeatSyncedWithTrack ? BeatSyncSource.Clock.Rate : 1; metronomeClock.Rate = IsBeatSyncedWithTrack ? BeatSyncSource.Clock.Rate : 1;
@ -259,7 +259,7 @@ namespace osu.Game.Screens.Edit.Timing
this.TransformBindableTo(interpolatedBpm, (int)Math.Round(timingPoint.BPM), 600, Easing.OutQuint); this.TransformBindableTo(interpolatedBpm, (int)Math.Round(timingPoint.BPM), 600, Easing.OutQuint);
} }
if (BeatSyncSource.Clock?.IsRunning != true && isSwinging) if (!BeatSyncSource.Clock.IsRunning && isSwinging)
{ {
swing.ClearTransforms(true); swing.ClearTransforms(true);

View File

@ -310,7 +310,7 @@ namespace osu.Game.Screens.Edit.Timing
} }
double averageBeatLength = (tapTimings.Last() - tapTimings.Skip(initial_taps_to_ignore).First()) / (tapTimings.Count - initial_taps_to_ignore - 1); double averageBeatLength = (tapTimings.Last() - tapTimings.Skip(initial_taps_to_ignore).First()) / (tapTimings.Count - initial_taps_to_ignore - 1);
double clockRate = beatSyncSource?.Clock?.Rate ?? 1; double clockRate = beatSyncSource?.Clock.Rate ?? 1;
double bpm = Math.Round(60000 / averageBeatLength / clockRate); double bpm = Math.Round(60000 / averageBeatLength / clockRate);

View File

@ -0,0 +1,67 @@
// 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.Track;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
namespace osu.Game.Screens.Menu
{
public partial class KiaiMenuFountains : BeatSyncedContainer
{
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.Both;
Children = new[]
{
new StarFountain
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
X = 250,
},
new StarFountain
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
X = -250,
},
};
}
private bool isTriggered;
private double? lastTrigger;
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
if (effectPoint.KiaiMode && !isTriggered)
{
bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - effectPoint.Time) < 500;
if (isNearEffectPoint)
Shoot();
}
isTriggered = effectPoint.KiaiMode;
}
public void Shoot()
{
if (lastTrigger != null && Clock.CurrentTime - lastTrigger < 500)
return;
foreach (var fountain in Children.OfType<StarFountain>())
fountain.Shoot();
lastTrigger = Clock.CurrentTime;
}
}
}

View File

@ -102,8 +102,7 @@ namespace osu.Game.Screens.Menu
for (int i = 0; i < temporalAmplitudes.Length; i++) for (int i = 0; i < temporalAmplitudes.Length; i++)
temporalAmplitudes[i] = 0; temporalAmplitudes[i] = 0;
if (beatSyncProvider.Clock != null) addAmplitudesFromSource(beatSyncProvider);
addAmplitudesFromSource(beatSyncProvider);
foreach (var source in amplitudeSources) foreach (var source in amplitudeSources)
addAmplitudesFromSource(source); addAmplitudesFromSource(source);

View File

@ -8,6 +8,7 @@ using System.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Logging; using osu.Framework.Logging;
@ -85,6 +86,7 @@ namespace osu.Game.Screens.Menu
private ParallaxContainer buttonsContainer; private ParallaxContainer buttonsContainer;
private SongTicker songTicker; private SongTicker songTicker;
private Container logoTarget;
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics) private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics)
@ -129,6 +131,7 @@ namespace osu.Game.Screens.Menu
} }
} }
}, },
logoTarget = new Container { RelativeSizeAxes = Axes.Both, },
sideFlashes = new MenuSideFlashes(), sideFlashes = new MenuSideFlashes(),
songTicker = new SongTicker songTicker = new SongTicker
{ {
@ -136,6 +139,7 @@ namespace osu.Game.Screens.Menu
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
Margin = new MarginPadding { Right = 15, Top = 5 } Margin = new MarginPadding { Right = 15, Top = 5 }
}, },
new KiaiMenuFountains(),
holdToExitGameOverlay?.CreateProxy() ?? Empty() holdToExitGameOverlay?.CreateProxy() ?? Empty()
}); });
@ -207,6 +211,8 @@ namespace osu.Game.Screens.Menu
logo.FadeColour(Color4.White, 100, Easing.OutQuint); logo.FadeColour(Color4.White, 100, Easing.OutQuint);
logo.FadeIn(100, Easing.OutQuint); logo.FadeIn(100, Easing.OutQuint);
logo.ProxyToContainer(logoTarget);
if (resuming) if (resuming)
{ {
Buttons.State = ButtonSystemState.TopLevel; Buttons.State = ButtonSystemState.TopLevel;
@ -244,6 +250,8 @@ namespace osu.Game.Screens.Menu
var seq = logo.FadeOut(300, Easing.InSine) var seq = logo.FadeOut(300, Easing.InSine)
.ScaleTo(0.2f, 300, Easing.InSine); .ScaleTo(0.2f, 300, Easing.InSine);
logo.ReturnProxy();
seq.OnComplete(_ => Buttons.SetOsuLogo(null)); seq.OnComplete(_ => Buttons.SetOsuLogo(null));
seq.OnAbort(_ => Buttons.SetOsuLogo(null)); seq.OnAbort(_ => Buttons.SetOsuLogo(null));
} }

View File

@ -435,5 +435,46 @@ namespace osu.Game.Screens.Menu
logoBounceContainer.MoveTo(Vector2.Zero, 800, Easing.OutElastic); logoBounceContainer.MoveTo(Vector2.Zero, 800, Easing.OutElastic);
base.OnDragEnd(e); base.OnDragEnd(e);
} }
private Container defaultProxyTarget;
private Container currentProxyTarget;
private Drawable proxy;
public Drawable ProxyToContainer(Container c)
{
if (currentProxyTarget != null)
throw new InvalidOperationException("Previous proxy usage was not returned");
if (defaultProxyTarget == null)
throw new InvalidOperationException($"{nameof(SetupDefaultContainer)} must be called first");
currentProxyTarget = c;
defaultProxyTarget.Remove(proxy, false);
currentProxyTarget.Add(proxy);
return proxy;
}
public void ReturnProxy()
{
if (currentProxyTarget == null)
throw new InvalidOperationException("No usage to return");
if (defaultProxyTarget == null)
throw new InvalidOperationException($"{nameof(SetupDefaultContainer)} must be called first");
currentProxyTarget.Remove(proxy, false);
currentProxyTarget = null;
defaultProxyTarget.Add(proxy);
}
public void SetupDefaultContainer(Container container)
{
defaultProxyTarget = container;
defaultProxyTarget.Add(this);
defaultProxyTarget.Add(proxy = CreateProxy());
}
} }
} }

View File

@ -0,0 +1,93 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics.Textures;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Screens.Menu
{
public partial class StarFountain : SkinReloadableDrawable
{
private StarFountainSpewer spewer = null!;
[Resolved]
private TextureStore textures { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
InternalChild = spewer = new StarFountainSpewer();
}
public void Shoot() => spewer.Shoot();
protected override void SkinChanged(ISkinSource skin)
{
base.SkinChanged(skin);
spewer.Texture = skin.GetTexture("Menu/fountain-star") ?? textures.Get("Menu/fountain-star");
}
public partial class StarFountainSpewer : ParticleSpewer
{
private const int particle_duration_min = 300;
private const int particle_duration_max = 1000;
private double? lastShootTime;
private int lastShootDirection;
protected override float ParticleGravity => 800;
private const double shoot_duration = 800;
protected override bool CanSpawnParticles => lastShootTime != null && Time.Current - lastShootTime < shoot_duration;
[Resolved]
private ISkinSource skin { get; set; } = null!;
public StarFountainSpewer()
: base(null, 240, particle_duration_max)
{
}
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
Texture = skin.GetTexture("Menu/fountain-star") ?? textures.Get("Menu/fountain-star");
Active.Value = true;
}
protected override FallingParticle CreateParticle()
{
return new FallingParticle
{
StartPosition = new Vector2(0, 50),
Duration = RNG.NextSingle(particle_duration_min, particle_duration_max),
StartAngle = getRandomVariance(4),
EndAngle = getRandomVariance(2),
EndScale = 2.2f + getRandomVariance(0.4f),
Velocity = new Vector2(getCurrentAngle(), -1400 + getRandomVariance(100)),
};
}
private float getCurrentAngle()
{
const float x_velocity_from_direction = 500;
const float x_velocity_random_variance = 60;
return lastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / shoot_duration) + getRandomVariance(x_velocity_random_variance);
}
public void Shoot()
{
lastShootTime = Clock.CurrentTime;
lastShootDirection = RNG.Next(-1, 2);
}
private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance);
}
}
}

View File

@ -22,6 +22,7 @@ using osu.Framework.Threading;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.IO.Archives; using osu.Game.IO.Archives;
@ -1170,6 +1171,7 @@ namespace osu.Game.Screens.Play
// because of the clone above, it's required that we copy back the post-import hash/ID to use for availability matching. // because of the clone above, it's required that we copy back the post-import hash/ID to use for availability matching.
score.ScoreInfo.Hash = s.Hash; score.ScoreInfo.Hash = s.Hash;
score.ScoreInfo.ID = s.ID; score.ScoreInfo.ID = s.ID;
score.ScoreInfo.Files.AddRange(s.Files.Detach());
}); });
return Task.CompletedTask; return Task.CompletedTask;

View File

@ -107,6 +107,9 @@ namespace osu.Game.Screens.Ranking
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{ {
if (e.Repeat)
return false;
switch (e.Action) switch (e.Action)
{ {
case GlobalAction.SaveReplay: case GlobalAction.SaveReplay:

View File

@ -352,29 +352,6 @@ namespace osu.Game.Screens.Select
if (working.Beatmap?.HitObjects.Any() != true) if (working.Beatmap?.HitObjects.Any() != true)
return; return;
infoLabelContainer.Children = new Drawable[]
{
new InfoLabel(new BeatmapStatistic
{
Name = "Length",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length),
Content = working.BeatmapInfo.Length.ToFormattedDuration().ToString(),
}),
bpmLabelContainer = new Container
{
AutoSizeAxes = Axes.Both,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(20, 0),
Children = getRulesetInfoLabels()
}
};
}
private InfoLabel[] getRulesetInfoLabels()
{
try try
{ {
IBeatmap playableBeatmap; IBeatmap playableBeatmap;
@ -390,14 +367,30 @@ namespace osu.Game.Screens.Select
playableBeatmap = working.GetPlayableBeatmap(working.BeatmapInfo.Ruleset, Array.Empty<Mod>()); playableBeatmap = working.GetPlayableBeatmap(working.BeatmapInfo.Ruleset, Array.Empty<Mod>());
} }
return playableBeatmap.GetStatistics().Select(s => new InfoLabel(s)).ToArray(); infoLabelContainer.Children = new Drawable[]
{
new InfoLabel(new BeatmapStatistic
{
Name = $"Length (Drain: {playableBeatmap.CalculateDrainLength().ToFormattedDuration().ToString()})",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length),
Content = working.BeatmapInfo.Length.ToFormattedDuration().ToString(),
}),
bpmLabelContainer = new Container
{
AutoSizeAxes = Axes.Both,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(20, 0),
Children = playableBeatmap.GetStatistics().Select(s => new InfoLabel(s)).ToArray()
}
};
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Error(e, "Could not load beatmap successfully!"); Logger.Error(e, "Could not load beatmap successfully!");
} }
return Array.Empty<InfoLabel>();
} }
private void refreshBPMLabel() private void refreshBPMLabel()

View File

@ -468,6 +468,13 @@ namespace osu.Game.Skinning
public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{ {
switch (componentName)
{
case "Menu/fountain-star":
componentName = "star2";
break;
}
foreach (string name in getFallbackNames(componentName)) foreach (string name in getFallbackNames(componentName))
{ {
// some component names (especially user-controlled ones, like `HitX` in mania) // some component names (especially user-controlled ones, like `HitX` in mania)

View File

@ -128,7 +128,7 @@ namespace osu.Game.Storyboards.Drawables
// //
// In the case of storyboard animations, we want to synchronise with game time perfectly // In the case of storyboard animations, we want to synchronise with game time perfectly
// so let's get a correct time based on gameplay clock and earliest transform. // so let's get a correct time based on gameplay clock and earliest transform.
PlaybackPosition = (beatSyncProvider.Clock?.CurrentTime ?? Clock.CurrentTime) - Animation.EarliestTransformTime; PlaybackPosition = beatSyncProvider.Clock.CurrentTime - Animation.EarliestTransformTime;
} }
private void skinSourceChanged() private void skinSourceChanged()

View File

@ -37,7 +37,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="11.1.2" /> <PackageReference Include="Realm" Version="11.1.2" />
<PackageReference Include="ppy.osu.Framework" Version="2023.724.0" /> <PackageReference Include="ppy.osu.Framework" Version="2023.724.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.707.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2023.719.0" />
<PackageReference Include="Sentry" Version="3.28.1" /> <PackageReference Include="Sentry" Version="3.28.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" /> <PackageReference Include="SharpCompress" Version="0.32.2" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />