mirror of
https://github.com/ppy/osu.git
synced 2024-12-15 21:03:08 +08:00
Merge branch 'master' into fix-cursor-in-scale-container
This commit is contained in:
commit
6b042eae94
@ -333,6 +333,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
Assert.AreEqual("hit_2.wav", getTestableSampleInfo(hitObjects[1]).LookupNames.First());
|
||||
Assert.AreEqual("normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First());
|
||||
Assert.AreEqual("hit_1.wav", getTestableSampleInfo(hitObjects[3]).LookupNames.First());
|
||||
Assert.AreEqual(70, getTestableSampleInfo(hitObjects[3]).Volume);
|
||||
}
|
||||
|
||||
SampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]);
|
||||
|
@ -13,4 +13,4 @@ SampleSet: Normal
|
||||
255,193,2170,1,0,0:0:0:0:hit_1.wav
|
||||
256,191,2638,5,0,0:0:0:0:hit_2.wav
|
||||
255,193,3107,1,0,0:0:0:0:
|
||||
256,191,3576,1,0,0:0:0:0:hit_1.wav
|
||||
256,191,3576,1,0,0:0:0:70:hit_1.wav
|
||||
|
@ -188,10 +188,10 @@ namespace osu.Game.Tests.Visual
|
||||
public void PauseTest()
|
||||
{
|
||||
performFullSetup(true);
|
||||
AddStep("Pause", () => player.CurrentPauseContainer.Pause());
|
||||
AddStep("Pause", () => player.CurrentPausableGameplayContainer.Pause());
|
||||
waitForDim();
|
||||
AddAssert("Screen is dimmed", () => songSelect.IsBackgroundDimmed());
|
||||
AddStep("Unpause", () => player.CurrentPauseContainer.Resume());
|
||||
AddStep("Unpause", () => player.CurrentPausableGameplayContainer.Resume());
|
||||
waitForDim();
|
||||
AddAssert("Screen is dimmed", () => songSelect.IsBackgroundDimmed());
|
||||
}
|
||||
@ -328,7 +328,7 @@ namespace osu.Game.Tests.Visual
|
||||
};
|
||||
}
|
||||
|
||||
public PauseContainer CurrentPauseContainer => PauseContainer;
|
||||
public PausableGameplayContainer CurrentPausableGameplayContainer => PausableGameplayContainer;
|
||||
|
||||
public UserDimContainer CurrentStoryboardContainer => StoryboardContainer;
|
||||
|
||||
|
47
osu.Game.Tests/Visual/TestCaseDirectPanel.cs
Normal file
47
osu.Game.Tests/Visual/TestCaseDirectPanel.cs
Normal file
@ -0,0 +1,47 @@
|
||||
// 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.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Overlays.Direct;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual
|
||||
{
|
||||
public class TestCaseDirectPanel : OsuTestCase
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
{
|
||||
typeof(DirectGridPanel),
|
||||
typeof(DirectListPanel),
|
||||
typeof(IconPill)
|
||||
};
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
var beatmap = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo, null);
|
||||
beatmap.BeatmapSetInfo.OnlineInfo.HasVideo = true;
|
||||
beatmap.BeatmapSetInfo.OnlineInfo.HasStoryboard = true;
|
||||
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Padding = new MarginPadding(20),
|
||||
Spacing = new Vector2(0, 20),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new DirectGridPanel(beatmap.BeatmapSetInfo),
|
||||
new DirectListPanel(beatmap.BeatmapSetInfo)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -17,15 +17,15 @@ namespace osu.Game.Tests.Visual
|
||||
[Description("player pause/fail screens")]
|
||||
public class TestCaseGameplayMenuOverlay : ManualInputManagerTestCase
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(FailOverlay), typeof(PauseContainer) };
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(FailOverlay), typeof(PausableGameplayContainer) };
|
||||
|
||||
private FailOverlay failOverlay;
|
||||
private PauseContainer.PauseOverlay pauseOverlay;
|
||||
private PausableGameplayContainer.PauseOverlay pauseOverlay;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Add(pauseOverlay = new PauseContainer.PauseOverlay
|
||||
Add(pauseOverlay = new PausableGameplayContainer.PauseOverlay
|
||||
{
|
||||
OnResume = () => Logger.Log(@"Resume"),
|
||||
OnRetry = () => Logger.Log(@"Retry"),
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.MathUtils;
|
||||
using osu.Framework.Timing;
|
||||
@ -19,14 +20,20 @@ namespace osu.Game.Tests.Visual
|
||||
|
||||
private readonly StopwatchClock clock;
|
||||
|
||||
[Cached]
|
||||
private readonly GameplayClock gameplayClock;
|
||||
|
||||
private readonly FramedClock framedClock;
|
||||
|
||||
public TestCaseSongProgress()
|
||||
{
|
||||
clock = new StopwatchClock(true);
|
||||
|
||||
gameplayClock = new GameplayClock(framedClock = new FramedClock(clock));
|
||||
|
||||
Add(progress = new SongProgress
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AudioClock = new StopwatchClock(true),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
});
|
||||
@ -68,8 +75,13 @@ namespace osu.Game.Tests.Visual
|
||||
progress.Objects = objects;
|
||||
graph.Objects = objects;
|
||||
|
||||
progress.AudioClock = clock;
|
||||
progress.OnSeek = pos => clock.Seek(pos);
|
||||
progress.RequestSeek = pos => clock.Seek(pos);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
framedClock.ProcessFrame();
|
||||
}
|
||||
|
||||
private class TestSongProgressGraph : SongProgressGraph
|
||||
|
@ -213,12 +213,6 @@ namespace osu.Game.Online.Leaderboards
|
||||
pendingUpdateScores?.Cancel();
|
||||
pendingUpdateScores = Schedule(() =>
|
||||
{
|
||||
if (api?.IsLoggedIn != true)
|
||||
{
|
||||
PlaceholderState = PlaceholderState.NotLoggedIn;
|
||||
return;
|
||||
}
|
||||
|
||||
PlaceholderState = PlaceholderState.Retrieving;
|
||||
loading.Show();
|
||||
|
||||
@ -231,6 +225,12 @@ namespace osu.Game.Online.Leaderboards
|
||||
if (getScoresRequest == null)
|
||||
return;
|
||||
|
||||
if (api?.IsLoggedIn != true)
|
||||
{
|
||||
PlaceholderState = PlaceholderState.NotLoggedIn;
|
||||
return;
|
||||
}
|
||||
|
||||
getScoresRequest.Failure += e => Schedule(() =>
|
||||
{
|
||||
if (e is OperationCanceledException)
|
||||
@ -243,6 +243,11 @@ namespace osu.Game.Online.Leaderboards
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a fetch/refresh of scores to be displayed.
|
||||
/// </summary>
|
||||
/// <param name="scoresCallback">A callback which should be called when fetching is completed. Scheduling is not required.</param>
|
||||
/// <returns>An <see cref="APIRequest"/> responsible for the fetch operation. This will be queued and performed automatically.</returns>
|
||||
protected abstract APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback);
|
||||
|
||||
private Placeholder currentPlaceholder;
|
||||
|
@ -13,6 +13,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
|
||||
namespace osu.Game.Overlays.Direct
|
||||
{
|
||||
@ -23,6 +24,7 @@ namespace osu.Game.Overlays.Direct
|
||||
private const float vertical_padding = 5;
|
||||
private const float height = 70;
|
||||
|
||||
private FillFlowContainer statusContainer;
|
||||
private PlayButton playButton;
|
||||
private Box progressBar;
|
||||
|
||||
@ -107,6 +109,18 @@ namespace osu.Game.Overlays.Direct
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
statusContainer = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding { Vertical = vertical_padding, Horizontal = 5 },
|
||||
Spacing = new Vector2(5),
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.X,
|
||||
Height = 20,
|
||||
@ -115,6 +129,8 @@ namespace osu.Game.Overlays.Direct
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
@ -194,6 +210,23 @@ namespace osu.Game.Overlays.Direct
|
||||
Colour = colours.Yellow,
|
||||
},
|
||||
});
|
||||
|
||||
if (SetInfo.OnlineInfo?.HasVideo ?? false)
|
||||
{
|
||||
statusContainer.Add(new IconPill(FontAwesome.fa_film) { IconSize = new Vector2(20) });
|
||||
}
|
||||
|
||||
if (SetInfo.OnlineInfo?.HasStoryboard ?? false)
|
||||
{
|
||||
statusContainer.Add(new IconPill(FontAwesome.fa_image) { IconSize = new Vector2(20) });
|
||||
}
|
||||
|
||||
statusContainer.Add(new BeatmapSetOnlineStatusPill
|
||||
{
|
||||
TextSize = 12,
|
||||
TextPadding = new MarginPadding { Horizontal = 10, Vertical = 4 },
|
||||
Status = SetInfo.OnlineInfo?.Status ?? BeatmapSetOnlineStatus.None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,14 @@ namespace osu.Game.Overlays.Direct
|
||||
{
|
||||
public class IconPill : CircularContainer
|
||||
{
|
||||
public Vector2 IconSize
|
||||
{
|
||||
get => iconContainer.Size;
|
||||
set => iconContainer.Size = value;
|
||||
}
|
||||
|
||||
private readonly Container iconContainer;
|
||||
|
||||
public IconPill(FontAwesome icon)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
@ -25,16 +33,16 @@ namespace osu.Game.Overlays.Direct
|
||||
Colour = Color4.Black,
|
||||
Alpha = 0.5f,
|
||||
},
|
||||
new Container
|
||||
iconContainer = new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding(5),
|
||||
Size = new Vector2(22),
|
||||
Padding = new MarginPadding(5),
|
||||
Child = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Icon = icon,
|
||||
Size = new Vector2(12),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -301,7 +301,16 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
{
|
||||
// Todo: This should return the normal SampleInfos if the specified sample file isn't found, but that's a pretty edge-case scenario
|
||||
if (!string.IsNullOrEmpty(bankInfo.Filename))
|
||||
return new List<SampleInfo> { new FileSampleInfo { Filename = bankInfo.Filename } };
|
||||
{
|
||||
return new List<SampleInfo>
|
||||
{
|
||||
new FileSampleInfo
|
||||
{
|
||||
Filename = bankInfo.Filename,
|
||||
Volume = bankInfo.Volume
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var soundTypes = new List<SampleInfo>
|
||||
{
|
||||
|
@ -41,6 +41,7 @@ namespace osu.Game.Rulesets.UI
|
||||
protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
|
||||
{
|
||||
InternalChild = KeyBindingContainer = CreateKeyBindingContainer(ruleset, variant, unique);
|
||||
gameplayClock = new GameplayClock(framedClock = new FramedClock(manualClock = new ManualClock()));
|
||||
}
|
||||
|
||||
#region Action mapping (for replays)
|
||||
@ -86,22 +87,28 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
#region Clock control
|
||||
|
||||
private ManualClock clock;
|
||||
private IFrameBasedClock parentClock;
|
||||
private readonly ManualClock manualClock;
|
||||
|
||||
private readonly FramedClock framedClock;
|
||||
|
||||
[Cached]
|
||||
private GameplayClock gameplayClock;
|
||||
|
||||
private IFrameBasedClock parentGameplayClock;
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OsuConfigManager config, GameplayClock clock)
|
||||
{
|
||||
mouseDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableButtons);
|
||||
|
||||
if (clock != null)
|
||||
parentGameplayClock = clock;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
//our clock will now be our parent's clock, but we want to replace this to allow manual control.
|
||||
parentClock = Clock;
|
||||
|
||||
ProcessCustomClock = false;
|
||||
Clock = new FramedClock(clock = new ManualClock
|
||||
{
|
||||
CurrentTime = parentClock.CurrentTime,
|
||||
Rate = parentClock.Rate,
|
||||
});
|
||||
setClock();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -147,25 +154,28 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
private void updateClock()
|
||||
{
|
||||
if (parentClock == null) return;
|
||||
if (parentGameplayClock == null)
|
||||
setClock(); // LoadComplete may not be run yet, but we still want the clock.
|
||||
|
||||
clock.Rate = parentClock.Rate;
|
||||
clock.IsRunning = parentClock.IsRunning;
|
||||
validState = true;
|
||||
|
||||
var newProposedTime = parentClock.CurrentTime;
|
||||
manualClock.Rate = parentGameplayClock.Rate;
|
||||
manualClock.IsRunning = parentGameplayClock.IsRunning;
|
||||
|
||||
var newProposedTime = parentGameplayClock.CurrentTime;
|
||||
|
||||
try
|
||||
{
|
||||
if (Math.Abs(clock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f)
|
||||
if (Math.Abs(manualClock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f)
|
||||
{
|
||||
newProposedTime = clock.Rate > 0
|
||||
? Math.Min(newProposedTime, clock.CurrentTime + sixty_frame_time)
|
||||
: Math.Max(newProposedTime, clock.CurrentTime - sixty_frame_time);
|
||||
newProposedTime = manualClock.Rate > 0
|
||||
? Math.Min(newProposedTime, manualClock.CurrentTime + sixty_frame_time)
|
||||
: Math.Max(newProposedTime, manualClock.CurrentTime - sixty_frame_time);
|
||||
}
|
||||
|
||||
if (!isAttached)
|
||||
{
|
||||
clock.CurrentTime = newProposedTime;
|
||||
manualClock.CurrentTime = newProposedTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -177,35 +187,39 @@ namespace osu.Game.Rulesets.UI
|
||||
validState = false;
|
||||
|
||||
requireMoreUpdateLoops = true;
|
||||
clock.CurrentTime = newProposedTime;
|
||||
manualClock.CurrentTime = newProposedTime;
|
||||
return;
|
||||
}
|
||||
|
||||
clock.CurrentTime = newTime.Value;
|
||||
manualClock.CurrentTime = newTime.Value;
|
||||
}
|
||||
|
||||
requireMoreUpdateLoops = clock.CurrentTime != parentClock.CurrentTime;
|
||||
requireMoreUpdateLoops = manualClock.CurrentTime != parentGameplayClock.CurrentTime;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// The manual clock time has changed in the above code. The framed clock now needs to be updated
|
||||
// to ensure that the its time is valid for our children before input is processed
|
||||
Clock.ProcessFrame();
|
||||
framedClock.ProcessFrame();
|
||||
}
|
||||
}
|
||||
|
||||
private void setClock()
|
||||
{
|
||||
// in case a parent gameplay clock isn't available, just use the parent clock.
|
||||
if (parentGameplayClock == null)
|
||||
parentGameplayClock = Clock;
|
||||
|
||||
Clock = gameplayClock;
|
||||
ProcessCustomClock = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Setting application (disables etc.)
|
||||
|
||||
private Bindable<bool> mouseDisabled;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
mouseDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableButtons);
|
||||
}
|
||||
|
||||
protected override bool Handle(UIEvent e)
|
||||
{
|
||||
switch (e)
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -40,13 +41,7 @@ namespace osu.Game.Screens.Play
|
||||
private readonly BreakInfo info;
|
||||
private readonly BreakArrows breakArrows;
|
||||
|
||||
public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor)
|
||||
: this(letterboxing)
|
||||
{
|
||||
bindProcessor(scoreProcessor);
|
||||
}
|
||||
|
||||
public BreakOverlay(bool letterboxing)
|
||||
public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor = null)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Child = fadeContainer = new Container
|
||||
@ -98,6 +93,14 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (scoreProcessor != null) bindProcessor(scoreProcessor);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(GameplayClock clock)
|
||||
{
|
||||
if (clock != null) Clock = clock;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
43
osu.Game/Screens/Play/GameplayClock.cs
Normal file
43
osu.Game/Screens/Play/GameplayClock.cs
Normal file
@ -0,0 +1,43 @@
|
||||
// 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.Timing;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
/// <summary>
|
||||
/// A clock which is used for gameplay elements that need to follow audio time 1:1.
|
||||
/// Exposed via DI by <see cref="PausableGameplayContainer"/>.
|
||||
/// <remarks>
|
||||
/// The main purpose of this clock is to stop components using it from accidentally processing the main
|
||||
/// <see cref="IFrameBasedClock"/>, as this should only be done once to ensure accuracy.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
public class GameplayClock : IFrameBasedClock
|
||||
{
|
||||
private readonly IFrameBasedClock underlyingClock;
|
||||
|
||||
public GameplayClock(IFrameBasedClock underlyingClock)
|
||||
{
|
||||
this.underlyingClock = underlyingClock;
|
||||
}
|
||||
|
||||
public double CurrentTime => underlyingClock.CurrentTime;
|
||||
|
||||
public double Rate => underlyingClock.Rate;
|
||||
|
||||
public bool IsRunning => underlyingClock.IsRunning;
|
||||
|
||||
public void ProcessFrame()
|
||||
{
|
||||
// we do not want to process the underlying clock.
|
||||
// this is handled by PauseContainer.
|
||||
}
|
||||
|
||||
public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime;
|
||||
|
||||
public double FramesPerSecond => underlyingClock.FramesPerSecond;
|
||||
|
||||
public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo;
|
||||
}
|
||||
}
|
@ -40,7 +40,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private static bool hasShownNotificationOnce;
|
||||
|
||||
public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working, IClock offsetClock, IAdjustableClock adjustableClock)
|
||||
public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working, IAdjustableClock adjustableClock)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
@ -81,7 +81,7 @@ namespace osu.Game.Screens.Play
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
KeyCounter = CreateKeyCounter(adjustableClock as IFrameBasedClock),
|
||||
KeyCounter = CreateKeyCounter(),
|
||||
HoldToQuit = CreateHoldForMenuButton(),
|
||||
}
|
||||
}
|
||||
@ -91,9 +91,8 @@ namespace osu.Game.Screens.Play
|
||||
BindRulesetContainer(rulesetContainer);
|
||||
|
||||
Progress.Objects = rulesetContainer.Objects;
|
||||
Progress.AudioClock = offsetClock;
|
||||
Progress.AllowSeeking = rulesetContainer.HasReplayLoaded.Value;
|
||||
Progress.OnSeek = pos => adjustableClock.Seek(pos);
|
||||
Progress.RequestSeek = pos => adjustableClock.Seek(pos);
|
||||
|
||||
ModDisplay.Current.BindTo(working.Mods);
|
||||
|
||||
@ -202,13 +201,12 @@ namespace osu.Game.Screens.Play
|
||||
Margin = new MarginPadding { Top = 20 }
|
||||
};
|
||||
|
||||
protected virtual KeyCounterCollection CreateKeyCounter(IFrameBasedClock offsetClock) => new KeyCounterCollection
|
||||
protected virtual KeyCounterCollection CreateKeyCounter() => new KeyCounterCollection
|
||||
{
|
||||
FadeTime = 50,
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Margin = new MarginPadding(10),
|
||||
AudioClock = offsetClock
|
||||
};
|
||||
|
||||
protected virtual SongProgress CreateProgress() => new SongProgress
|
||||
|
@ -71,9 +71,12 @@ namespace osu.Game.Screens.Play
|
||||
Name = name;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(TextureStore textures)
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(TextureStore textures, GameplayClock clock)
|
||||
{
|
||||
if (clock != null)
|
||||
Clock = clock;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
buttonSprite = new Sprite
|
||||
|
@ -8,7 +8,6 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Configuration;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@ -37,9 +36,6 @@ namespace osu.Game.Screens.Play
|
||||
key.FadeTime = FadeTime;
|
||||
key.KeyDownTextColor = KeyDownTextColor;
|
||||
key.KeyUpTextColor = KeyUpTextColor;
|
||||
// Use the same clock object as SongProgress for saving KeyCounter state
|
||||
if (AudioClock != null)
|
||||
key.Clock = AudioClock;
|
||||
}
|
||||
|
||||
public void ResetCount()
|
||||
@ -125,8 +121,6 @@ namespace osu.Game.Screens.Play
|
||||
public override bool HandleNonPositionalInput => receptor == null;
|
||||
public override bool HandlePositionalInput => receptor == null;
|
||||
|
||||
public IFrameBasedClock AudioClock { get; set; }
|
||||
|
||||
private Receptor receptor;
|
||||
|
||||
public Receptor GetReceptor()
|
||||
|
@ -14,10 +14,11 @@ using osuTK.Graphics;
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
/// <summary>
|
||||
/// A container which handles pausing children, displaying a pause overlay with choices etc.
|
||||
/// A container which handles pausing children, displaying a pause overlay with choices and processing the clock.
|
||||
/// Exposes a <see cref="GameplayClock"/> to children via DI.
|
||||
/// This alleviates a lot of the intricate pause logic from being in <see cref="Player"/>
|
||||
/// </summary>
|
||||
public class PauseContainer : Container
|
||||
public class PausableGameplayContainer : Container
|
||||
{
|
||||
public readonly BindableBool IsPaused = new BindableBool();
|
||||
|
||||
@ -43,24 +44,32 @@ namespace osu.Game.Screens.Play
|
||||
public Action OnRetry;
|
||||
public Action OnQuit;
|
||||
|
||||
private readonly FramedClock framedClock;
|
||||
private readonly DecoupleableInterpolatingFramedClock decoupledClock;
|
||||
private readonly FramedClock offsetClock;
|
||||
private readonly DecoupleableInterpolatingFramedClock adjustableClock;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="PauseContainer"/>.
|
||||
/// The final clock which is exposed to underlying components.
|
||||
/// </summary>
|
||||
/// <param name="framedClock">The gameplay clock. This is the clock that will process frames.</param>
|
||||
/// <param name="decoupledClock">The seekable clock. This is the clock that will be paused and resumed.</param>
|
||||
public PauseContainer(FramedClock framedClock, DecoupleableInterpolatingFramedClock decoupledClock)
|
||||
[Cached]
|
||||
private readonly GameplayClock gameplayClock;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="PausableGameplayContainer"/>.
|
||||
/// </summary>
|
||||
/// <param name="offsetClock">The gameplay clock. This is the clock that will process frames. Includes user/system offsets.</param>
|
||||
/// <param name="adjustableClock">The seekable clock. This is the clock that will be paused and resumed. Should not be processed (it is processed automatically by <see cref="offsetClock"/>).</param>
|
||||
public PausableGameplayContainer(FramedClock offsetClock, DecoupleableInterpolatingFramedClock adjustableClock)
|
||||
{
|
||||
this.framedClock = framedClock;
|
||||
this.decoupledClock = decoupledClock;
|
||||
this.offsetClock = offsetClock;
|
||||
this.adjustableClock = adjustableClock;
|
||||
|
||||
gameplayClock = new GameplayClock(offsetClock);
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
AddInternal(content = new Container
|
||||
{
|
||||
Clock = this.framedClock,
|
||||
Clock = this.offsetClock,
|
||||
ProcessCustomClock = false,
|
||||
RelativeSizeAxes = Axes.Both
|
||||
});
|
||||
@ -84,7 +93,7 @@ namespace osu.Game.Screens.Play
|
||||
if (IsPaused.Value) return;
|
||||
|
||||
// stop the seekable clock (stops the audio eventually)
|
||||
decoupledClock.Stop();
|
||||
adjustableClock.Stop();
|
||||
IsPaused.Value = true;
|
||||
|
||||
pauseOverlay.Show();
|
||||
@ -102,8 +111,8 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
// Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time
|
||||
// This accounts for the audio clock source potentially taking time to enter a completely stopped state
|
||||
decoupledClock.Seek(decoupledClock.CurrentTime);
|
||||
decoupledClock.Start();
|
||||
adjustableClock.Seek(adjustableClock.CurrentTime);
|
||||
adjustableClock.Start();
|
||||
|
||||
pauseOverlay.Hide();
|
||||
}
|
||||
@ -123,7 +132,7 @@ namespace osu.Game.Screens.Play
|
||||
Pause();
|
||||
|
||||
if (!IsPaused.Value)
|
||||
framedClock.ProcessFrame();
|
||||
offsetClock.ProcessFrame();
|
||||
|
||||
base.Update();
|
||||
}
|
@ -67,7 +67,7 @@ namespace osu.Game.Screens.Play
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; }
|
||||
|
||||
protected PauseContainer PauseContainer { get; private set; }
|
||||
protected PausableGameplayContainer PausableGameplayContainer { get; private set; }
|
||||
|
||||
private RulesetInfo ruleset;
|
||||
|
||||
@ -170,10 +170,10 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
PauseContainer = new PauseContainer(offsetClock, adjustableClock)
|
||||
PausableGameplayContainer = new PausableGameplayContainer(offsetClock, adjustableClock)
|
||||
{
|
||||
Retries = RestartCount,
|
||||
OnRetry = Restart,
|
||||
OnRetry = restart,
|
||||
OnQuit = performUserRequestedExit,
|
||||
CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded.Value,
|
||||
Children = new Container[]
|
||||
@ -191,32 +191,26 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
ProcessCustomClock = false,
|
||||
Breaks = beatmap.Breaks
|
||||
},
|
||||
new ScalingContainer(ScalingMode.Gameplay)
|
||||
{
|
||||
Child = RulesetContainer.Cursor?.CreateProxy() ?? new Container(),
|
||||
},
|
||||
HUDOverlay = new HUDOverlay(ScoreProcessor, RulesetContainer, working, offsetClock, adjustableClock)
|
||||
HUDOverlay = new HUDOverlay(ScoreProcessor, RulesetContainer, working, adjustableClock)
|
||||
{
|
||||
Clock = Clock, // hud overlay doesn't want to use the audio clock directly
|
||||
ProcessCustomClock = false,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
},
|
||||
new SkipOverlay(RulesetContainer.GameplayStartTime)
|
||||
{
|
||||
Clock = Clock, // skip button doesn't want to use the audio clock directly
|
||||
ProcessCustomClock = false,
|
||||
AdjustableClock = adjustableClock,
|
||||
FramedClock = offsetClock,
|
||||
RequestSeek = time => adjustableClock.Seek(time)
|
||||
},
|
||||
}
|
||||
},
|
||||
failOverlay = new FailOverlay
|
||||
{
|
||||
OnRetry = Restart,
|
||||
OnRetry = restart,
|
||||
OnQuit = performUserRequestedExit,
|
||||
},
|
||||
new HotkeyRetryOverlay
|
||||
@ -226,7 +220,7 @@ namespace osu.Game.Screens.Play
|
||||
if (!this.IsCurrentScreen()) return;
|
||||
|
||||
fadeOut(true);
|
||||
Restart();
|
||||
restart();
|
||||
},
|
||||
}
|
||||
};
|
||||
@ -234,7 +228,7 @@ namespace osu.Game.Screens.Play
|
||||
HUDOverlay.HoldToQuit.Action = performUserRequestedExit;
|
||||
HUDOverlay.KeyCounter.Visible.BindTo(RulesetContainer.HasReplayLoaded);
|
||||
|
||||
RulesetContainer.IsPaused.BindTo(PauseContainer.IsPaused);
|
||||
RulesetContainer.IsPaused.BindTo(PausableGameplayContainer.IsPaused);
|
||||
|
||||
if (ShowStoryboard.Value)
|
||||
initializeStoryboard(false);
|
||||
@ -263,7 +257,7 @@ namespace osu.Game.Screens.Play
|
||||
this.Exit();
|
||||
}
|
||||
|
||||
public void Restart()
|
||||
private void restart()
|
||||
{
|
||||
if (!this.IsCurrentScreen()) return;
|
||||
|
||||
@ -367,7 +361,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
this.Delay(750).Schedule(() =>
|
||||
{
|
||||
if (!PauseContainer.IsPaused.Value)
|
||||
if (!PausableGameplayContainer.IsPaused.Value)
|
||||
{
|
||||
adjustableClock.Start();
|
||||
}
|
||||
@ -375,8 +369,8 @@ namespace osu.Game.Screens.Play
|
||||
});
|
||||
});
|
||||
|
||||
PauseContainer.Alpha = 0;
|
||||
PauseContainer.FadeIn(750, Easing.OutQuint);
|
||||
PausableGameplayContainer.Alpha = 0;
|
||||
PausableGameplayContainer.FadeIn(750, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public override void OnSuspending(IScreen next)
|
||||
@ -394,7 +388,7 @@ namespace osu.Game.Screens.Play
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((!AllowPause || HasFailed || !ValidForResume || PauseContainer?.IsPaused.Value != false || RulesetContainer?.HasReplayLoaded.Value != false) && (!PauseContainer?.IsResuming ?? true))
|
||||
if ((!AllowPause || HasFailed || !ValidForResume || PausableGameplayContainer?.IsPaused.Value != false || RulesetContainer?.HasReplayLoaded.Value != false) && (!PausableGameplayContainer?.IsResuming ?? true))
|
||||
{
|
||||
// In the case of replays, we may have changed the playback rate.
|
||||
applyRateFromMods();
|
||||
@ -403,7 +397,7 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
|
||||
if (LoadedBeatmapSuccessfully)
|
||||
PauseContainer?.Pause();
|
||||
PausableGameplayContainer?.Pause();
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -417,7 +411,7 @@ namespace osu.Game.Screens.Play
|
||||
storyboardReplacesBackground.Value = false;
|
||||
}
|
||||
|
||||
protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !PauseContainer.IsPaused.Value;
|
||||
protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !PausableGameplayContainer.IsPaused.Value;
|
||||
|
||||
private void initializeStoryboard(bool asyncLoad)
|
||||
{
|
||||
|
@ -17,6 +17,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Play.PlayerSettings;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@ -296,6 +297,7 @@ namespace osu.Game.Screens.Play
|
||||
private readonly WorkingBeatmap beatmap;
|
||||
private LoadingAnimation loading;
|
||||
private Sprite backgroundSprite;
|
||||
private ModDisplay modDisplay;
|
||||
|
||||
public bool Loading
|
||||
{
|
||||
@ -322,7 +324,7 @@ namespace osu.Game.Screens.Play
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
var metadata = beatmap?.BeatmapInfo?.Metadata ?? new BeatmapMetadata();
|
||||
var metadata = beatmap.BeatmapInfo?.Metadata ?? new BeatmapMetadata();
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Children = new Drawable[]
|
||||
@ -391,6 +393,14 @@ namespace osu.Game.Screens.Play
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
},
|
||||
new ModDisplay
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding { Top = 20 },
|
||||
Current = beatmap.Mods
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
@ -9,7 +9,6 @@ using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Screens.Ranking;
|
||||
@ -27,8 +26,7 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
private readonly double startTime;
|
||||
|
||||
public IAdjustableClock AdjustableClock;
|
||||
public IFrameBasedClock FramedClock;
|
||||
public Action<double> RequestSeek;
|
||||
|
||||
private Button button;
|
||||
private Box remainingTimeBox;
|
||||
@ -54,16 +52,13 @@ namespace osu.Game.Screens.Play
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OsuColour colours, GameplayClock clock)
|
||||
{
|
||||
var baseClock = Clock;
|
||||
|
||||
if (FramedClock != null)
|
||||
{
|
||||
Clock = FramedClock;
|
||||
ProcessCustomClock = false;
|
||||
}
|
||||
if (clock != null)
|
||||
Clock = clock;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@ -111,7 +106,7 @@ namespace osu.Game.Screens.Play
|
||||
using (BeginAbsoluteSequence(beginFadeTime))
|
||||
this.FadeOut(fade_time);
|
||||
|
||||
button.Action = () => AdjustableClock?.Seek(startTime - skip_required_cutoff - fade_time);
|
||||
button.Action = () => RequestSeek?.Invoke(startTime - skip_required_cutoff - fade_time);
|
||||
|
||||
displayTime = Time.Current;
|
||||
|
||||
|
@ -10,7 +10,6 @@ using osu.Game.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@ -29,18 +28,11 @@ namespace osu.Game.Screens.Play
|
||||
private readonly SongProgressGraph graph;
|
||||
private readonly SongProgressInfo info;
|
||||
|
||||
public Action<double> OnSeek;
|
||||
public Action<double> RequestSeek;
|
||||
|
||||
public override bool HandleNonPositionalInput => AllowSeeking;
|
||||
public override bool HandlePositionalInput => AllowSeeking;
|
||||
|
||||
private IClock audioClock;
|
||||
|
||||
public IClock AudioClock
|
||||
{
|
||||
set => audioClock = info.AudioClock = value;
|
||||
}
|
||||
|
||||
private double lastHitTime => ((objects.Last() as IHasEndTime)?.EndTime ?? objects.Last().StartTime) + 1;
|
||||
|
||||
private double firstHitTime => objects.First().StartTime;
|
||||
@ -63,9 +55,14 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private readonly BindableBool replayLoaded = new BindableBool();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
private GameplayClock gameplayClock;
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OsuColour colours, GameplayClock clock)
|
||||
{
|
||||
if (clock != null)
|
||||
gameplayClock = clock;
|
||||
|
||||
graph.FillColour = bar.FillColour = colours.BlueLighter;
|
||||
}
|
||||
|
||||
@ -99,7 +96,7 @@ namespace osu.Game.Screens.Play
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
OnSeek = position => OnSeek?.Invoke(position),
|
||||
OnSeek = time => RequestSeek?.Invoke(time),
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -157,14 +154,11 @@ namespace osu.Game.Screens.Play
|
||||
if (objects == null)
|
||||
return;
|
||||
|
||||
double position = audioClock?.CurrentTime ?? Time.Current;
|
||||
double progress = (position - firstHitTime) / (lastHitTime - firstHitTime);
|
||||
double position = gameplayClock?.CurrentTime ?? Time.Current;
|
||||
double progress = Math.Min(1, (position - firstHitTime) / (lastHitTime - firstHitTime));
|
||||
|
||||
if (progress < 1)
|
||||
{
|
||||
bar.CurrentTime = position;
|
||||
graph.Progress = (int)(graph.ColumnCount * progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using System;
|
||||
@ -27,8 +26,6 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private const int margin = 10;
|
||||
|
||||
public IClock AudioClock;
|
||||
|
||||
public double StartTime
|
||||
{
|
||||
set => startTime = value;
|
||||
@ -39,9 +36,14 @@ namespace osu.Game.Screens.Play
|
||||
set => endTime = value;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
private GameplayClock gameplayClock;
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OsuColour colours, GameplayClock clock)
|
||||
{
|
||||
if (clock != null)
|
||||
gameplayClock = clock;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
timeCurrent = new OsuSpriteText
|
||||
@ -80,7 +82,9 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
base.Update();
|
||||
|
||||
double songCurrentTime = AudioClock.CurrentTime - startTime;
|
||||
var time = gameplayClock?.CurrentTime ?? Time.Current;
|
||||
|
||||
double songCurrentTime = time - startTime;
|
||||
int currentPercent = Math.Max(0, Math.Min(100, (int)(songCurrentTime / songLength * 100)));
|
||||
int currentSecond = (int)Math.Floor(songCurrentTime / 1000.0);
|
||||
|
||||
@ -93,7 +97,7 @@ namespace osu.Game.Screens.Play
|
||||
if (currentSecond != previousSecond && songCurrentTime < songLength)
|
||||
{
|
||||
timeCurrent.Text = formatTime(TimeSpan.FromSeconds(currentSecond));
|
||||
timeLeft.Text = formatTime(TimeSpan.FromMilliseconds(endTime - AudioClock.CurrentTime));
|
||||
timeLeft.Text = formatTime(TimeSpan.FromMilliseconds(endTime - time));
|
||||
|
||||
previousSecond = currentSecond;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -21,6 +22,18 @@ namespace osu.Game.Tests.Beatmaps
|
||||
HitObjects = baseBeatmap.HitObjects;
|
||||
|
||||
BeatmapInfo.Ruleset = ruleset;
|
||||
BeatmapInfo.BeatmapSet.Metadata = BeatmapInfo.Metadata;
|
||||
BeatmapInfo.BeatmapSet.Beatmaps = new List<BeatmapInfo> { BeatmapInfo };
|
||||
BeatmapInfo.BeatmapSet.OnlineInfo = new BeatmapSetOnlineInfo
|
||||
{
|
||||
Status = BeatmapSetOnlineStatus.Ranked,
|
||||
Covers = new BeatmapSetOnlineCovers
|
||||
{
|
||||
Cover = "https://assets.ppy.sh/beatmaps/163112/covers/cover.jpg",
|
||||
Card = "https://assets.ppy.sh/beatmaps/163112/covers/card.jpg",
|
||||
List = "https://assets.ppy.sh/beatmaps/163112/covers/list.jpg"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Beatmap createTestBeatmap()
|
||||
|
Loading…
Reference in New Issue
Block a user