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

Merge branch 'master' into beatmap-collection-inteface-types

This commit is contained in:
Dean Herbert 2021-11-16 15:31:10 +09:00
commit 726a0cc091
28 changed files with 1067 additions and 161 deletions

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1026.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.1112.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1108.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.1108.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">

View File

@ -10,9 +10,13 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
using osuTK.Graphics;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
@ -58,6 +62,42 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null); AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
AddStep("exit player", () => editorPlayer.Exit()); AddStep("exit player", () => editorPlayer.Exit());
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor); AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
AddUntilStep("background has correct params", () =>
{
var background = this.ChildrenOfType<BackgroundScreenBeatmap>().Single();
return background.Colour == Color4.DarkGray && background.BlurAmount.Value == 0;
});
}
[Test]
public void TestGameplayTestWhenTrackRunning()
{
AddStep("start track", () => EditorClock.Start());
AddAssert("sample playback enabled", () => !Editor.SamplePlaybackDisabled.Value);
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
AddAssert("editor track stopped", () => !EditorClock.IsRunning);
AddAssert("sample playback disabled", () => Editor.SamplePlaybackDisabled.Value);
AddStep("exit player", () => editorPlayer.Exit());
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
AddUntilStep("background has correct params", () =>
{
var background = this.ChildrenOfType<BackgroundScreenBeatmap>().Single();
return background.Colour == Color4.DarkGray && background.BlurAmount.Value == 0;
});
AddStep("start track", () => EditorClock.Start());
AddAssert("sample playback re-enabled", () => !Editor.SamplePlaybackDisabled.Value);
} }
[Test] [Test]
@ -111,6 +151,35 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("track stopped", () => !Beatmap.Value.Track.IsRunning); AddAssert("track stopped", () => !Beatmap.Value.Track.IsRunning);
} }
[Test]
public void TestSharedClockState()
{
AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000));
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
GameplayClockContainer gameplayClockContainer = null;
AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType<GameplayClockContainer>().First());
AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning);
AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000);
double timeAtPlayerExit = 0;
AddWaitStep("wait some", 5);
AddStep("store time before exit", () => timeAtPlayerExit = gameplayClockContainer.CurrentTime);
AddStep("exit player", () => editorPlayer.Exit());
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
AddAssert("time is past player exit", () => EditorClock.CurrentTime >= timeAtPlayerExit);
}
public override void TearDownSteps() public override void TearDownSteps()
{ {
base.TearDownSteps(); base.TearDownSteps();

View File

@ -0,0 +1,171 @@
// 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 Humanizer;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneHitObjectDifficultyPointAdjustments : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("add test objects", () =>
{
EditorBeatmap.Add(new Slider
{
StartTime = 0,
Position = (OsuPlayfield.BASE_SIZE - new Vector2(0, 100)) / 2,
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(0, 100))
}
}
});
EditorBeatmap.Add(new Slider
{
StartTime = 500,
Position = (OsuPlayfield.BASE_SIZE - new Vector2(100, 0)) / 2,
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(100, 0))
}
},
DifficultyControlPoint = new DifficultyControlPoint
{
SliderVelocity = 2
}
});
});
}
[Test]
public void TestSingleSelection()
{
clickDifficultyPiece(0);
velocityPopoverHasSingleValue(1);
dismissPopover();
// select first object to ensure that difficulty pieces for unselected objects
// work independently from selection state.
AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.First()));
clickDifficultyPiece(1);
velocityPopoverHasSingleValue(2);
setVelocityViaPopover(5);
hitObjectHasVelocity(1, 5);
}
[Test]
public void TestMultipleSelectionWithSameSliderVelocity()
{
AddStep("unify slider velocity", () =>
{
foreach (var h in EditorBeatmap.HitObjects)
h.DifficultyControlPoint.SliderVelocity = 1.5;
});
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickDifficultyPiece(0);
velocityPopoverHasSingleValue(1.5);
dismissPopover();
clickDifficultyPiece(1);
velocityPopoverHasSingleValue(1.5);
setVelocityViaPopover(5);
hitObjectHasVelocity(0, 5);
hitObjectHasVelocity(1, 5);
}
[Test]
public void TestMultipleSelectionWithDifferentSliderVelocity()
{
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickDifficultyPiece(0);
velocityPopoverHasIndeterminateValue();
dismissPopover();
clickDifficultyPiece(1);
velocityPopoverHasIndeterminateValue();
setVelocityViaPopover(3);
hitObjectHasVelocity(0, 3);
hitObjectHasVelocity(1, 3);
}
private void clickDifficultyPiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} difficulty piece", () =>
{
var difficultyPiece = this.ChildrenOfType<DifficultyPointPiece>().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
InputManager.MoveMouseTo(difficultyPiece);
InputManager.Click(MouseButton.Left);
});
private void velocityPopoverHasSingleValue(double velocity) => AddUntilStep($"velocity popover has {velocity}", () =>
{
var popover = this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().SingleOrDefault();
var slider = popover?.ChildrenOfType<IndeterminateSliderWithTextBoxInput<double>>().Single();
return slider?.Current.Value == velocity;
});
private void velocityPopoverHasIndeterminateValue() => AddUntilStep("velocity popover has indeterminate value", () =>
{
var popover = this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().SingleOrDefault();
var slider = popover?.ChildrenOfType<IndeterminateSliderWithTextBoxInput<double>>().Single();
return slider != null && slider.Current.Value == null;
});
private void dismissPopover()
{
AddStep("dismiss popover", () => InputManager.Key(Key.Escape));
AddUntilStep("wait for dismiss", () => !this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().Any(popover => popover.IsPresent));
}
private void setVelocityViaPopover(double velocity) => AddStep($"set {velocity} via popover", () =>
{
var popover = this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().Single();
var slider = popover.ChildrenOfType<IndeterminateSliderWithTextBoxInput<double>>().Single();
slider.Current.Value = velocity;
});
private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
return h.DifficultyControlPoint.SliderVelocity == velocity;
});
}
}

View File

@ -0,0 +1,252 @@
// 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 Humanizer;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneHitObjectSamplePointAdjustments : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("add test objects", () =>
{
EditorBeatmap.Add(new HitCircle
{
StartTime = 0,
Position = (OsuPlayfield.BASE_SIZE - new Vector2(100, 0)) / 2,
SampleControlPoint = new SampleControlPoint
{
SampleBank = "normal",
SampleVolume = 80
}
});
EditorBeatmap.Add(new HitCircle
{
StartTime = 500,
Position = (OsuPlayfield.BASE_SIZE + new Vector2(100, 0)) / 2,
SampleControlPoint = new SampleControlPoint
{
SampleBank = "soft",
SampleVolume = 60
}
});
});
}
[Test]
public void TestSingleSelection()
{
clickSamplePiece(0);
samplePopoverHasSingleBank("normal");
samplePopoverHasSingleVolume(80);
dismissPopover();
// select first object to ensure that sample pieces for unselected objects
// work independently from selection state.
AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.First()));
clickSamplePiece(1);
samplePopoverHasSingleBank("soft");
samplePopoverHasSingleVolume(60);
setVolumeViaPopover(90);
hitObjectHasSampleVolume(1, 90);
setBankViaPopover("drum");
hitObjectHasSampleBank(1, "drum");
}
[Test]
public void TestMultipleSelectionWithSameSampleVolume()
{
AddStep("unify sample volume", () =>
{
foreach (var h in EditorBeatmap.HitObjects)
h.SampleControlPoint.SampleVolume = 50;
});
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickSamplePiece(0);
samplePopoverHasSingleVolume(50);
dismissPopover();
clickSamplePiece(1);
samplePopoverHasSingleVolume(50);
setVolumeViaPopover(75);
hitObjectHasSampleVolume(0, 75);
hitObjectHasSampleVolume(1, 75);
}
[Test]
public void TestMultipleSelectionWithDifferentSampleVolume()
{
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickSamplePiece(0);
samplePopoverHasIndeterminateVolume();
dismissPopover();
clickSamplePiece(1);
samplePopoverHasIndeterminateVolume();
setVolumeViaPopover(30);
hitObjectHasSampleVolume(0, 30);
hitObjectHasSampleVolume(1, 30);
}
[Test]
public void TestMultipleSelectionWithSameSampleBank()
{
AddStep("unify sample bank", () =>
{
foreach (var h in EditorBeatmap.HitObjects)
h.SampleControlPoint.SampleBank = "soft";
});
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickSamplePiece(0);
samplePopoverHasSingleBank("soft");
dismissPopover();
clickSamplePiece(1);
samplePopoverHasSingleBank("soft");
setBankViaPopover(string.Empty);
hitObjectHasSampleBank(0, "soft");
hitObjectHasSampleBank(1, "soft");
samplePopoverHasSingleBank("soft");
setBankViaPopover("drum");
hitObjectHasSampleBank(0, "drum");
hitObjectHasSampleBank(1, "drum");
samplePopoverHasSingleBank("drum");
}
[Test]
public void TestMultipleSelectionWithDifferentSampleBank()
{
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickSamplePiece(0);
samplePopoverHasIndeterminateBank();
dismissPopover();
clickSamplePiece(1);
samplePopoverHasIndeterminateBank();
setBankViaPopover(string.Empty);
hitObjectHasSampleBank(0, "normal");
hitObjectHasSampleBank(1, "soft");
samplePopoverHasIndeterminateBank();
setBankViaPopover("normal");
hitObjectHasSampleBank(0, "normal");
hitObjectHasSampleBank(1, "normal");
samplePopoverHasSingleBank("normal");
}
private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} difficulty piece", () =>
{
var difficultyPiece = this.ChildrenOfType<SamplePointPiece>().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
InputManager.MoveMouseTo(difficultyPiece);
InputManager.Click(MouseButton.Left);
});
private void samplePopoverHasSingleVolume(int volume) => AddUntilStep($"sample popover has volume {volume}", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
var slider = popover?.ChildrenOfType<IndeterminateSliderWithTextBoxInput<int>>().Single();
return slider?.Current.Value == volume;
});
private void samplePopoverHasIndeterminateVolume() => AddUntilStep("sample popover has indeterminate volume", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
var slider = popover?.ChildrenOfType<IndeterminateSliderWithTextBoxInput<int>>().Single();
return slider != null && slider.Current.Value == null;
});
private void samplePopoverHasSingleBank(string bank) => AddUntilStep($"sample popover has bank {bank}", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
var textBox = popover?.ChildrenOfType<OsuTextBox>().First();
return textBox?.Current.Value == bank && string.IsNullOrEmpty(textBox?.PlaceholderText.ToString());
});
private void samplePopoverHasIndeterminateBank() => AddUntilStep("sample popover has indeterminate bank", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
var textBox = popover?.ChildrenOfType<OsuTextBox>().First();
return textBox != null && string.IsNullOrEmpty(textBox.Current.Value) && !string.IsNullOrEmpty(textBox.PlaceholderText.ToString());
});
private void dismissPopover()
{
AddStep("dismiss popover", () => InputManager.Key(Key.Escape));
AddUntilStep("wait for dismiss", () => !this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().Any(popover => popover.IsPresent));
}
private void setVolumeViaPopover(int volume) => AddStep($"set volume {volume} via popover", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().Single();
var slider = popover.ChildrenOfType<IndeterminateSliderWithTextBoxInput<int>>().Single();
slider.Current.Value = volume;
});
private void hitObjectHasSampleVolume(int objectIndex, int volume) => AddAssert($"{objectIndex.ToOrdinalWords()} has volume {volume}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
return h.SampleControlPoint.SampleVolume == volume;
});
private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().Single();
var textBox = popover.ChildrenOfType<LabelledTextBox>().First();
textBox.Current.Value = bank;
// force a commit via keyboard.
// this is needed when testing attempting to set empty bank - which should revert to the previous value, but only on commit.
InputManager.ChangeFocus(textBox);
InputManager.Key(Key.Enter);
});
private void hitObjectHasSampleBank(int objectIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} has bank {bank}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
return h.SampleControlPoint.SampleBank == bank;
});
}
}

View File

@ -31,12 +31,18 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
AddUntilStep("wait for fail", () => Player.HasFailed); AddUntilStep("wait for fail", () => Player.HasFailed);
AddUntilStep("wait for fail overlay", () => ((FailPlayer)Player).FailOverlay.State.Value == Visibility.Visible); AddUntilStep("wait for fail overlay", () => ((FailPlayer)Player).FailOverlay.State.Value == Visibility.Visible);
// The pause screen and fail animation both ramp frequency.
// This tests to ensure that it doesn't reset during that handoff.
AddAssert("frequency only ever decreased", () => !((FailPlayer)Player).FrequencyIncreased);
} }
private class FailPlayer : TestPlayer private class FailPlayer : TestPlayer
{ {
public new FailOverlay FailOverlay => base.FailOverlay; public new FailOverlay FailOverlay => base.FailOverlay;
public bool FrequencyIncreased { get; private set; }
public FailPlayer() public FailPlayer()
: base(false, false) : base(false, false)
{ {
@ -47,6 +53,19 @@ namespace osu.Game.Tests.Visual.Gameplay
base.LoadComplete(); base.LoadComplete();
HealthProcessor.FailConditions += (_, __) => true; HealthProcessor.FailConditions += (_, __) => true;
} }
private double lastFrequency = double.MaxValue;
protected override void Update()
{
base.Update();
double freq = Beatmap.Value.Track.AggregateFrequency.Value;
FrequencyIncreased |= freq > lastFrequency;
lastFrequency = freq;
}
} }
} }
} }

View File

@ -17,33 +17,69 @@ namespace osu.Game.Beatmaps
{ {
public interface IWorkingBeatmap public interface IWorkingBeatmap
{ {
IBeatmapInfo BeatmapInfo { get; }
IBeatmapSetInfo BeatmapSetInfo { get; }
IBeatmapMetadataInfo Metadata { get; }
/// <summary> /// <summary>
/// Retrieves the <see cref="IBeatmap"/> which this <see cref="WorkingBeatmap"/> represents. /// Whether the Beatmap has finished loading.
///</summary>
public bool BeatmapLoaded { get; }
/// <summary>
/// Whether the Background has finished loading.
///</summary>
public bool BackgroundLoaded { get; }
/// <summary>
/// Whether the Waveform has finished loading.
///</summary>
public bool WaveformLoaded { get; }
/// <summary>
/// Whether the Storyboard has finished loading.
///</summary>
public bool StoryboardLoaded { get; }
/// <summary>
/// Whether the Skin has finished loading.
///</summary>
public bool SkinLoaded { get; }
/// <summary>
/// Whether the Track has finished loading.
///</summary>
public bool TrackLoaded { get; }
/// <summary>
/// Retrieves the <see cref="IBeatmap"/> which this <see cref="IWorkingBeatmap"/> represents.
/// </summary> /// </summary>
IBeatmap Beatmap { get; } IBeatmap Beatmap { get; }
/// <summary> /// <summary>
/// Retrieves the background for this <see cref="WorkingBeatmap"/>. /// Retrieves the background for this <see cref="IWorkingBeatmap"/>.
/// </summary> /// </summary>
Texture Background { get; } Texture Background { get; }
/// <summary> /// <summary>
/// Retrieves the <see cref="Waveform"/> for the <see cref="Track"/> of this <see cref="WorkingBeatmap"/>. /// Retrieves the <see cref="Waveform"/> for the <see cref="Track"/> of this <see cref="IWorkingBeatmap"/>.
/// </summary> /// </summary>
Waveform Waveform { get; } Waveform Waveform { get; }
/// <summary> /// <summary>
/// Retrieves the <see cref="Storyboard"/> which this <see cref="WorkingBeatmap"/> provides. /// Retrieves the <see cref="Storyboard"/> which this <see cref="IWorkingBeatmap"/> provides.
/// </summary> /// </summary>
Storyboard Storyboard { get; } Storyboard Storyboard { get; }
/// <summary> /// <summary>
/// Retrieves the <see cref="Skin"/> which this <see cref="WorkingBeatmap"/> provides. /// Retrieves the <see cref="Skin"/> which this <see cref="IWorkingBeatmap"/> provides.
/// </summary> /// </summary>
ISkin Skin { get; } ISkin Skin { get; }
/// <summary> /// <summary>
/// Retrieves the <see cref="Track"/> which this <see cref="WorkingBeatmap"/> has loaded. /// Retrieves the <see cref="Track"/> which this <see cref="IWorkingBeatmap"/> has loaded.
/// </summary> /// </summary>
Track Track { get; } Track Track { get; }
@ -67,7 +103,7 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// In a standard game context, the loading of the track is managed solely by MusicController, which will /// In a standard game context, the loading of the track is managed solely by MusicController, which will
/// automatically load the track of the current global IBindable WorkingBeatmap. /// automatically load the track of the current global IBindable IWorkingBeatmap.
/// As such, this method should only be called in very special scenarios, such as external tests or apps which are /// As such, this method should only be called in very special scenarios, such as external tests or apps which are
/// outside of the game context. /// outside of the game context.
/// </remarks> /// </remarks>
@ -79,5 +115,20 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
/// <param name="storagePath">The storage path to the file.</param> /// <param name="storagePath">The storage path to the file.</param>
Stream GetStream(string storagePath); Stream GetStream(string storagePath);
/// <summary>
/// Beings loading the contents of this <see cref="IWorkingBeatmap"/> asynchronously.
/// </summary>
public void BeginAsyncLoad();
/// <summary>
/// Cancels the asynchronous loading of the contents of this <see cref="IWorkingBeatmap"/>.
/// </summary>
public void CancelAsyncLoad();
/// <summary>
/// Reads the correct track restart point from beatmap metadata and sets looping to enabled.
/// </summary>
void PrepareTrackForPreviewLooping();
} }
} }

View File

@ -27,9 +27,7 @@ namespace osu.Game.Beatmaps
public abstract class WorkingBeatmap : IWorkingBeatmap public abstract class WorkingBeatmap : IWorkingBeatmap
{ {
public readonly BeatmapInfo BeatmapInfo; public readonly BeatmapInfo BeatmapInfo;
public readonly BeatmapSetInfo BeatmapSetInfo; public readonly BeatmapSetInfo BeatmapSetInfo;
public readonly BeatmapMetadata Metadata; public readonly BeatmapMetadata Metadata;
protected AudioManager AudioManager { get; } protected AudioManager AudioManager { get; }
@ -89,6 +87,9 @@ namespace osu.Game.Beatmaps
var rulesetInstance = ruleset.CreateInstance(); var rulesetInstance = ruleset.CreateInstance();
if (rulesetInstance == null)
throw new RulesetLoadException("Creating ruleset instance failed when attempting to create playable beatmap.");
IBeatmapConverter converter = CreateBeatmapConverter(Beatmap, rulesetInstance); IBeatmapConverter converter = CreateBeatmapConverter(Beatmap, rulesetInstance);
// Check if the beatmap can be converted // Check if the beatmap can be converted
@ -176,17 +177,8 @@ namespace osu.Game.Beatmaps
private CancellationTokenSource loadCancellation = new CancellationTokenSource(); private CancellationTokenSource loadCancellation = new CancellationTokenSource();
/// <summary> public void BeginAsyncLoad() => loadBeatmapAsync();
/// Beings loading the contents of this <see cref="WorkingBeatmap"/> asynchronously.
/// </summary>
public void BeginAsyncLoad()
{
loadBeatmapAsync();
}
/// <summary>
/// Cancels the asynchronous loading of the contents of this <see cref="WorkingBeatmap"/>.
/// </summary>
public void CancelAsyncLoad() public void CancelAsyncLoad()
{ {
lock (beatmapFetchLock) lock (beatmapFetchLock)
@ -234,6 +226,10 @@ namespace osu.Game.Beatmaps
public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false; public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
IBeatmapInfo IWorkingBeatmap.BeatmapInfo => BeatmapInfo;
IBeatmapMetadataInfo IWorkingBeatmap.Metadata => Metadata;
IBeatmapSetInfo IWorkingBeatmap.BeatmapSetInfo => BeatmapSetInfo;
public IBeatmap Beatmap public IBeatmap Beatmap
{ {
get get
@ -273,9 +269,6 @@ namespace osu.Game.Beatmaps
[NotNull] [NotNull]
public Track LoadTrack() => loadedTrack = GetBeatmapTrack() ?? GetVirtualTrack(1000); public Track LoadTrack() => loadedTrack = GetBeatmapTrack() ?? GetVirtualTrack(1000);
/// <summary>
/// Reads the correct track restart point from beatmap metadata and sets looping to enabled.
/// </summary>
public void PrepareTrackForPreviewLooping() public void PrepareTrackForPreviewLooping()
{ {
Track.Looping = true; Track.Looping = true;

View File

@ -47,10 +47,30 @@ namespace osu.Game.Database
/// </summary> /// </summary>
public ArchiveReader GetReader() public ArchiveReader GetReader()
{ {
if (Stream != null) return Stream != null
return new ZipArchiveReader(Stream, Path); ? getReaderFrom(Stream)
: getReaderFrom(Path);
}
return getReaderFrom(Path); /// <summary>
/// Creates an <see cref="ArchiveReader"/> from a stream.
/// </summary>
/// <param name="stream">A seekable stream containing the archive content.</param>
/// <returns>A reader giving access to the archive's content.</returns>
private ArchiveReader getReaderFrom(Stream stream)
{
if (!(stream is MemoryStream memoryStream))
{
// This isn't used in any current path. May need to reconsider for performance reasons (ie. if we don't expect the incoming stream to be copied out).
byte[] buffer = new byte[stream.Length];
stream.Read(buffer, 0, (int)stream.Length);
memoryStream = new MemoryStream(buffer);
}
if (ZipUtils.IsZipArchive(memoryStream))
return new ZipArchiveReader(memoryStream, Path);
return new LegacyByteArrayReader(memoryStream.ToArray(), Path);
} }
/// <summary> /// <summary>

View File

@ -52,15 +52,18 @@ namespace osu.Game.Graphics
public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(new[] public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(new[]
{ {
(1.5f, Color4Extensions.FromHex("4fc0ff")), (0.1f, Color4Extensions.FromHex("aaaaaa")),
(0.1f, Color4Extensions.FromHex("4290fb")),
(1.25f, Color4Extensions.FromHex("4fc0ff")),
(2.0f, Color4Extensions.FromHex("4fffd5")), (2.0f, Color4Extensions.FromHex("4fffd5")),
(2.5f, Color4Extensions.FromHex("7cff4f")), (2.5f, Color4Extensions.FromHex("7cff4f")),
(3.25f, Color4Extensions.FromHex("f6f05c")), (3.3f, Color4Extensions.FromHex("f6f05c")),
(4.5f, Color4Extensions.FromHex("ff8068")), (4.2f, Color4Extensions.FromHex("ff8068")),
(6.0f, Color4Extensions.FromHex("ff3c71")), (4.9f, Color4Extensions.FromHex("ff4e6f")),
(7.0f, Color4Extensions.FromHex("6563de")), (5.8f, Color4Extensions.FromHex("c645b8")),
(8.0f, Color4Extensions.FromHex("18158e")), (6.7f, Color4Extensions.FromHex("6563de")),
(8.0f, Color4.Black), (7.7f, Color4Extensions.FromHex("18158e")),
(9.0f, Color4.Black),
}, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero)); }, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero));
/// <summary> /// <summary>

View File

@ -19,6 +19,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Utils;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
@ -219,7 +220,7 @@ namespace osu.Game.Graphics.UserInterface
decimal decimalPrecision = normalise(CurrentNumber.Precision.ToDecimal(NumberFormatInfo.InvariantInfo), max_decimal_digits); decimal decimalPrecision = normalise(CurrentNumber.Precision.ToDecimal(NumberFormatInfo.InvariantInfo), max_decimal_digits);
// Find the number of significant digits (we could have less than 5 after normalize()) // Find the number of significant digits (we could have less than 5 after normalize())
int significantDigits = findPrecision(decimalPrecision); int significantDigits = FormatUtils.FindPrecision(decimalPrecision);
TooltipText = floatValue.ToString($"N{significantDigits}"); TooltipText = floatValue.ToString($"N{significantDigits}");
} }
@ -248,23 +249,5 @@ namespace osu.Game.Graphics.UserInterface
/// <returns>The normalised decimal.</returns> /// <returns>The normalised decimal.</returns>
private decimal normalise(decimal d, int sd) private decimal normalise(decimal d, int sd)
=> decimal.Parse(Math.Round(d, sd).ToString(string.Concat("0.", new string('#', sd)), CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); => decimal.Parse(Math.Round(d, sd).ToString(string.Concat("0.", new string('#', sd)), CultureInfo.InvariantCulture), CultureInfo.InvariantCulture);
/// <summary>
/// Finds the number of digits after the decimal.
/// </summary>
/// <param name="d">The value to find the number of decimal digits for.</param>
/// <returns>The number decimal digits.</returns>
private int findPrecision(decimal d)
{
int precision = 0;
while (d != Math.Round(d))
{
d *= 10;
precision++;
}
return precision;
}
} }
} }

View File

@ -7,7 +7,7 @@ using System.IO;
namespace osu.Game.IO.Archives namespace osu.Game.IO.Archives
{ {
/// <summary> /// <summary>
/// Allows reading a single file from the provided stream. /// Allows reading a single file from the provided byte array.
/// </summary> /// </summary>
public class LegacyByteArrayReader : ArchiveReader public class LegacyByteArrayReader : ArchiveReader
{ {

View File

@ -6,7 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
{ {
@ -29,36 +29,26 @@ namespace osu.Game.Overlays.Settings
Children = new Drawable[] Children = new Drawable[]
{ {
new FillFlowContainer new OsuTextFlowContainer
{ {
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical, Padding = new MarginPadding
Children = new Drawable[]
{ {
new OsuSpriteText Horizontal = SettingsPanel.CONTENT_MARGINS,
{ Top = Toolbar.Toolbar.TOOLTIP_HEIGHT,
Text = heading, Bottom = 30
Font = OsuFont.TorusAlternate.With(size: 40),
Margin = new MarginPadding
{
Left = SettingsPanel.CONTENT_MARGINS,
Top = Toolbar.Toolbar.TOOLTIP_HEIGHT
},
},
new OsuSpriteText
{
Colour = colourProvider.Content2,
Text = subheading,
Font = OsuFont.GetFont(size: 18),
Margin = new MarginPadding
{
Left = SettingsPanel.CONTENT_MARGINS,
Bottom = 30
},
},
} }
} }.With(flow =>
{
flow.AddText(heading, header => header.Font = OsuFont.TorusAlternate.With(size: 40));
flow.NewLine();
flow.AddText(subheading, subheader =>
{
subheader.Colour = colourProvider.Content2;
subheader.Font = OsuFont.GetFont(size: 18);
});
})
}; };
} }
} }

View File

@ -1,9 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
@ -14,19 +16,20 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Timing;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
public class DifficultyPointPiece : HitObjectPointPiece, IHasPopover public class DifficultyPointPiece : HitObjectPointPiece, IHasPopover
{ {
private readonly HitObject hitObject; public readonly HitObject HitObject;
private readonly BindableNumber<double> speedMultiplier; private readonly BindableNumber<double> speedMultiplier;
public DifficultyPointPiece(HitObject hitObject) public DifficultyPointPiece(HitObject hitObject)
: base(hitObject.DifficultyControlPoint) : base(hitObject.DifficultyControlPoint)
{ {
this.hitObject = hitObject; HitObject = hitObject;
speedMultiplier = hitObject.DifficultyControlPoint.SliderVelocityBindable.GetBoundCopy(); speedMultiplier = hitObject.DifficultyControlPoint.SliderVelocityBindable.GetBoundCopy();
} }
@ -44,14 +47,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return true; return true;
} }
public Popover GetPopover() => new DifficultyEditPopover(hitObject); public Popover GetPopover() => new DifficultyEditPopover(HitObject);
public class DifficultyEditPopover : OsuPopover public class DifficultyEditPopover : OsuPopover
{ {
private readonly HitObject hitObject; private readonly HitObject hitObject;
private readonly DifficultyControlPoint point;
private SliderWithTextBoxInput<double> sliderVelocitySlider; private IndeterminateSliderWithTextBoxInput<double> sliderVelocitySlider;
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private EditorBeatmap beatmap { get; set; } private EditorBeatmap beatmap { get; set; }
@ -59,7 +61,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public DifficultyEditPopover(HitObject hitObject) public DifficultyEditPopover(HitObject hitObject)
{ {
this.hitObject = hitObject; this.hitObject = hitObject;
point = hitObject.DifficultyControlPoint;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -72,11 +73,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Width = 200, Width = 200,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0, 15),
Children = new Drawable[] Children = new Drawable[]
{ {
sliderVelocitySlider = new SliderWithTextBoxInput<double>("Velocity") sliderVelocitySlider = new IndeterminateSliderWithTextBoxInput<double>("Velocity", new DifficultyControlPoint().SliderVelocityBindable)
{ {
Current = new DifficultyControlPoint().SliderVelocityBindable,
KeyboardStep = 0.1f KeyboardStep = 0.1f
}, },
new OsuTextFlowContainer new OsuTextFlowContainer
@ -89,17 +90,37 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
} }
}; };
var selectedPointBindable = point.SliderVelocityBindable; // if the piece belongs to a currently selected object, assume that the user wants to change all selected objects.
// if the piece belongs to an unselected object, operate on that object alone, independently of the selection.
var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray();
var relevantControlPoints = relevantObjects.Select(h => h.DifficultyControlPoint).ToArray();
// there may be legacy control points, which contain infinite precision for compatibility reasons (see LegacyDifficultyControlPoint). // even if there are multiple objects selected, we can still display a value if they all have the same value.
// generally that level of precision could only be set by externally editing the .osu file, so at the point var selectedPointBindable = relevantControlPoints.Select(point => point.SliderVelocity).Distinct().Count() == 1 ? relevantControlPoints.First().SliderVelocityBindable : null;
// a user is looking to update this within the editor it should be safe to obliterate this additional precision.
double expectedPrecision = new DifficultyControlPoint().SliderVelocityBindable.Precision;
if (selectedPointBindable.Precision < expectedPrecision)
selectedPointBindable.Precision = expectedPrecision;
sliderVelocitySlider.Current = selectedPointBindable; if (selectedPointBindable != null)
sliderVelocitySlider.Current.BindValueChanged(_ => beatmap?.Update(hitObject)); {
// there may be legacy control points, which contain infinite precision for compatibility reasons (see LegacyDifficultyControlPoint).
// generally that level of precision could only be set by externally editing the .osu file, so at the point
// a user is looking to update this within the editor it should be safe to obliterate this additional precision.
sliderVelocitySlider.Current.Value = selectedPointBindable.Value;
}
sliderVelocitySlider.Current.BindValueChanged(val =>
{
if (val.NewValue == null)
return;
beatmap.BeginChange();
foreach (var h in relevantObjects)
{
h.DifficultyControlPoint.SliderVelocity = val.NewValue.Value;
beatmap.Update(h);
}
beatmap.EndChange();
});
} }
} }
} }

View File

@ -1,9 +1,14 @@
// 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.
#nullable enable
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
@ -14,12 +19,13 @@ using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Timing;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
public class SamplePointPiece : HitObjectPointPiece, IHasPopover public class SamplePointPiece : HitObjectPointPiece, IHasPopover
{ {
private readonly HitObject hitObject; public readonly HitObject HitObject;
private readonly Bindable<string> bank; private readonly Bindable<string> bank;
private readonly BindableNumber<int> volume; private readonly BindableNumber<int> volume;
@ -27,7 +33,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public SamplePointPiece(HitObject hitObject) public SamplePointPiece(HitObject hitObject)
: base(hitObject.SampleControlPoint) : base(hitObject.SampleControlPoint)
{ {
this.hitObject = hitObject; HitObject = hitObject;
volume = hitObject.SampleControlPoint.SampleVolumeBindable.GetBoundCopy(); volume = hitObject.SampleControlPoint.SampleVolumeBindable.GetBoundCopy();
bank = hitObject.SampleControlPoint.SampleBankBindable.GetBoundCopy(); bank = hitObject.SampleControlPoint.SampleBankBindable.GetBoundCopy();
} }
@ -50,23 +56,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Label.Text = $"{bank.Value} {volume.Value}"; Label.Text = $"{bank.Value} {volume.Value}";
} }
public Popover GetPopover() => new SampleEditPopover(hitObject); public Popover GetPopover() => new SampleEditPopover(HitObject);
public class SampleEditPopover : OsuPopover public class SampleEditPopover : OsuPopover
{ {
private readonly HitObject hitObject; private readonly HitObject hitObject;
private readonly SampleControlPoint point;
private LabelledTextBox bank; private LabelledTextBox bank = null!;
private SliderWithTextBoxInput<int> volume; private IndeterminateSliderWithTextBoxInput<int> volume = null!;
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private EditorBeatmap beatmap { get; set; } private EditorBeatmap beatmap { get; set; } = null!;
public SampleEditPopover(HitObject hitObject) public SampleEditPopover(HitObject hitObject)
{ {
this.hitObject = hitObject; this.hitObject = hitObject;
point = hitObject.SampleControlPoint;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -79,25 +83,84 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Width = 200, Width = 200,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0, 10),
Children = new Drawable[] Children = new Drawable[]
{ {
bank = new LabelledTextBox bank = new LabelledTextBox
{ {
Label = "Bank Name", Label = "Bank Name",
}, },
volume = new SliderWithTextBoxInput<int>("Volume") volume = new IndeterminateSliderWithTextBoxInput<int>("Volume", new SampleControlPoint().SampleVolumeBindable)
{
Current = new SampleControlPoint().SampleVolumeBindable,
}
} }
} }
}; };
bank.Current = point.SampleBankBindable; // if the piece belongs to a currently selected object, assume that the user wants to change all selected objects.
bank.Current.BindValueChanged(_ => beatmap.Update(hitObject)); // if the piece belongs to an unselected object, operate on that object alone, independently of the selection.
var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray();
var relevantControlPoints = relevantObjects.Select(h => h.SampleControlPoint).ToArray();
volume.Current = point.SampleVolumeBindable; // even if there are multiple objects selected, we can still display sample volume or bank if they all have the same value.
volume.Current.BindValueChanged(_ => beatmap.Update(hitObject)); string? commonBank = getCommonBank(relevantControlPoints);
if (!string.IsNullOrEmpty(commonBank))
bank.Current.Value = commonBank;
int? commonVolume = getCommonVolume(relevantControlPoints);
if (commonVolume != null)
volume.Current.Value = commonVolume.Value;
updateBankPlaceholderText(relevantObjects);
bank.Current.BindValueChanged(val =>
{
updateBankFor(relevantObjects, val.NewValue);
updateBankPlaceholderText(relevantObjects);
});
// on commit, ensure that the value is correct by sourcing it from the objects' control points again.
// this ensures that committing empty text causes a revert to the previous value.
bank.OnCommit += (_, __) => bank.Current.Value = getCommonBank(relevantControlPoints);
volume.Current.BindValueChanged(val => updateVolumeFor(relevantObjects, val.NewValue));
}
private static string? getCommonBank(SampleControlPoint[] relevantControlPoints) => relevantControlPoints.Select(point => point.SampleBank).Distinct().Count() == 1 ? relevantControlPoints.First().SampleBank : null;
private static int? getCommonVolume(SampleControlPoint[] relevantControlPoints) => relevantControlPoints.Select(point => point.SampleVolume).Distinct().Count() == 1 ? (int?)relevantControlPoints.First().SampleVolume : null;
private void updateBankFor(IEnumerable<HitObject> objects, string? newBank)
{
if (string.IsNullOrEmpty(newBank))
return;
beatmap.BeginChange();
foreach (var h in objects)
{
h.SampleControlPoint.SampleBank = newBank;
beatmap.Update(h);
}
beatmap.EndChange();
}
private void updateBankPlaceholderText(IEnumerable<HitObject> objects)
{
string? commonBank = getCommonBank(objects.Select(h => h.SampleControlPoint).ToArray());
bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : null;
}
private void updateVolumeFor(IEnumerable<HitObject> objects, int? newVolume)
{
if (newVolume == null)
return;
beatmap.BeginChange();
foreach (var h in objects)
{
h.SampleControlPoint.SampleVolume = newVolume.Value;
beatmap.Update(h);
}
beatmap.EndChange();
} }
} }
} }

View File

@ -31,6 +31,7 @@ using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Design; using osu.Game.Screens.Edit.Design;
using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Timing;
using osu.Game.Screens.Edit.Verify; using osu.Game.Screens.Edit.Verify;
@ -324,6 +325,19 @@ namespace osu.Game.Screens.Edit
/// </summary> /// </summary>
public void UpdateClockSource() => clock.ChangeSource(Beatmap.Value.Track); public void UpdateClockSource() => clock.ChangeSource(Beatmap.Value.Track);
/// <summary>
/// Creates an <see cref="EditorState"/> instance representing the current state of the editor.
/// </summary>
/// <param name="nextBeatmap">
/// The next beatmap to be shown, in the case of difficulty switch.
/// <see langword="null"/> indicates that the beatmap will not be changing.
/// </param>
public EditorState GetState([CanBeNull] BeatmapInfo nextBeatmap = null) => new EditorState
{
Time = clock.CurrentTimeAccurate,
ClipboardContent = nextBeatmap == null || editorBeatmap.BeatmapInfo.RulesetID == nextBeatmap.RulesetID ? Clipboard.Content.Value : string.Empty
};
/// <summary> /// <summary>
/// Restore the editor to a provided state. /// Restore the editor to a provided state.
/// </summary> /// </summary>
@ -486,7 +500,18 @@ namespace osu.Game.Screens.Edit
public override void OnEntering(IScreen last) public override void OnEntering(IScreen last)
{ {
base.OnEntering(last); base.OnEntering(last);
dimBackground();
resetTrack(true);
}
public override void OnResuming(IScreen last)
{
base.OnResuming(last);
dimBackground();
}
private void dimBackground()
{
ApplyToBackground(b => ApplyToBackground(b =>
{ {
// todo: temporary. we want to be applying dim using the UserDimContainer eventually. // todo: temporary. we want to be applying dim using the UserDimContainer eventually.
@ -495,8 +520,6 @@ namespace osu.Game.Screens.Edit
b.IgnoreUserSettings.Value = true; b.IgnoreUserSettings.Value = true;
b.BlurAmount.Value = 0; b.BlurAmount.Value = 0;
}); });
resetTrack(true);
} }
public override bool OnExiting(IScreen next) public override bool OnExiting(IScreen next)
@ -535,9 +558,9 @@ namespace osu.Game.Screens.Edit
public override void OnSuspending(IScreen next) public override void OnSuspending(IScreen next)
{ {
refetchBeatmap();
base.OnSuspending(next); base.OnSuspending(next);
clock.Stop();
refetchBeatmap();
} }
private void refetchBeatmap() private void refetchBeatmap()
@ -770,11 +793,7 @@ namespace osu.Game.Screens.Edit
return new DifficultyMenuItem(beatmapInfo, isCurrentDifficulty, SwitchToDifficulty); return new DifficultyMenuItem(beatmapInfo, isCurrentDifficulty, SwitchToDifficulty);
} }
protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleDifficultySwitch(nextBeatmap, new EditorState protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleDifficultySwitch(nextBeatmap, GetState(nextBeatmap));
{
Time = clock.CurrentTimeAccurate,
ClipboardContent = editorBeatmap.BeatmapInfo.RulesetID == nextBeatmap.RulesetID ? Clipboard.Content.Value : string.Empty
});
private void cancelExit() private void cancelExit()
{ {
@ -797,7 +816,7 @@ namespace osu.Game.Screens.Edit
pushEditorPlayer(); pushEditorPlayer();
} }
void pushEditorPlayer() => this.Push(new PlayerLoader(() => new EditorPlayer())); void pushEditorPlayer() => this.Push(new EditorPlayerLoader(this));
} }
public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);

View File

@ -3,21 +3,30 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
namespace osu.Game.Screens.Edit namespace osu.Game.Screens.Edit.GameplayTest
{ {
public class EditorPlayer : Player public class EditorPlayer : Player
{ {
public EditorPlayer() private readonly Editor editor;
: base(new PlayerConfiguration { ShowResults = false }) private readonly EditorState editorState;
{
}
[Resolved] [Resolved]
private MusicController musicController { get; set; } private MusicController musicController { get; set; }
public EditorPlayer(Editor editor)
: base(new PlayerConfiguration { ShowResults = false })
{
this.editor = editor;
editorState = editor.GetState();
}
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
=> new MasterGameplayClockContainer(beatmap, editorState.Time, true);
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -35,9 +44,22 @@ namespace osu.Game.Screens.Edit
protected override bool CheckModsAllowFailure() => false; // never fail. protected override bool CheckModsAllowFailure() => false; // never fail.
public override void OnEntering(IScreen last)
{
base.OnEntering(last);
// finish alpha transforms on entering to avoid gameplay starting in a half-hidden state.
// the finish calls are purposefully not propagated to children to avoid messing up their state.
FinishTransforms();
GameplayClockContainer.FinishTransforms(false, nameof(Alpha));
}
public override bool OnExiting(IScreen next) public override bool OnExiting(IScreen next)
{ {
musicController.Stop(); musicController.Stop();
editorState.Time = GameplayClockContainer.CurrentTime;
editor.RestoreState(editorState);
return base.OnExiting(next); return base.OnExiting(next);
} }
} }

View File

@ -0,0 +1,44 @@
// 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;
using osu.Framework.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.Edit.GameplayTest
{
public class EditorPlayerLoader : PlayerLoader
{
[Resolved]
private OsuLogo osuLogo { get; set; }
public EditorPlayerLoader(Editor editor)
: base(() => new EditorPlayer(editor))
{
}
public override void OnEntering(IScreen last)
{
base.OnEntering(last);
MetadataInfo.FinishTransforms(true);
}
protected override void LogoArriving(OsuLogo logo, bool resuming)
{
// call base with resuming forcefully set to true to reduce logo movements.
base.LogoArriving(logo, true);
logo.FinishTransforms(true, nameof(Scale));
}
protected override void ContentOut()
{
base.ContentOut();
osuLogo.FadeOut(CONTENT_OUT_DURATION, Easing.OutQuint);
}
protected override double PlayerPushDelay => 0;
}
}

View File

@ -5,7 +5,7 @@ using System;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
namespace osu.Game.Screens.Edit namespace osu.Game.Screens.Edit.GameplayTest
{ {
public class SaveBeforeGameplayTestDialog : PopupDialog public class SaveBeforeGameplayTestDialog : PopupDialog
{ {

View File

@ -0,0 +1,123 @@
// 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.Globalization;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays.Settings;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Screens.Edit.Timing
{
/// <summary>
/// Analogous to <see cref="SliderWithTextBoxInput{T}"/>, but supports scenarios
/// where multiple objects with multiple different property values are selected
/// by providing an "indeterminate state".
/// </summary>
public class IndeterminateSliderWithTextBoxInput<T> : CompositeDrawable, IHasCurrentValue<T?>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
{
/// <summary>
/// A custom step value for each key press which actuates a change on this control.
/// </summary>
public float KeyboardStep
{
get => slider.KeyboardStep;
set => slider.KeyboardStep = value;
}
private readonly BindableWithCurrent<T?> current = new BindableWithCurrent<T?>();
public Bindable<T?> Current
{
get => current.Current;
set => current.Current = value;
}
private readonly SettingsSlider<T> slider;
private readonly LabelledTextBox textbox;
/// <summary>
/// Creates an <see cref="IndeterminateSliderWithTextBoxInput{T}"/>.
/// </summary>
/// <param name="labelText">The label text for the slider and text box.</param>
/// <param name="indeterminateValue">
/// Bindable to use for the slider until a non-null value is set for <see cref="Current"/>.
/// In particular, it can be used to control min/max bounds and precision in the case of <see cref="BindableNumber{T}"/>s.
/// </param>
public IndeterminateSliderWithTextBoxInput(LocalisableString labelText, Bindable<T> indeterminateValue)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
Children = new Drawable[]
{
textbox = new LabelledTextBox
{
Label = labelText,
},
slider = new SettingsSlider<T>
{
TransferValueOnCommit = true,
RelativeSizeAxes = Axes.X,
Current = indeterminateValue
}
}
},
};
textbox.OnCommit += (t, isNew) =>
{
if (!isNew) return;
try
{
slider.Current.Parse(t.Text);
}
catch
{
// TriggerChange below will restore the previous text value on failure.
}
// This is run regardless of parsing success as the parsed number may not actually trigger a change
// due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state.
Current.TriggerChange();
};
slider.Current.BindValueChanged(val => Current.Value = val.NewValue);
Current.BindValueChanged(_ => updateState(), true);
}
private void updateState()
{
if (Current.Value is T nonNullValue)
{
slider.Current.Value = nonNullValue;
// use the value from the slider to ensure that any precision/min/max set on it via the initial indeterminate value have been applied correctly.
decimal decimalValue = slider.Current.Value.ToDecimal(NumberFormatInfo.InvariantInfo);
textbox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}");
textbox.PlaceholderText = string.Empty;
}
else
{
textbox.Text = null;
textbox.PlaceholderText = "(multiple)";
}
}
}
}

View File

@ -107,7 +107,8 @@ namespace osu.Game.Screens.Play
this.TransformBindableTo(trackFreq, 0, duration).OnComplete(_ => this.TransformBindableTo(trackFreq, 0, duration).OnComplete(_ =>
{ {
RemoveFilters(); // Don't reset frequency as the pause screen may appear post transform, causing a second frequency sweep.
RemoveFilters(false);
OnComplete?.Invoke(); OnComplete?.Invoke();
}); });
@ -137,15 +138,16 @@ namespace osu.Game.Screens.Play
Content.FadeColour(Color4.Gray, duration); Content.FadeColour(Color4.Gray, duration);
} }
public void RemoveFilters() public void RemoveFilters(bool resetTrackFrequency = true)
{ {
if (resetTrackFrequency)
track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq);
if (filters.Parent == null) if (filters.Parent == null)
return; return;
RemoveInternal(filters); RemoveInternal(filters);
filters.Dispose(); filters.Dispose();
track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq);
} }
protected override void Update() protected override void Update()

View File

@ -5,6 +5,7 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using ManagedBass.Fx;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -35,7 +36,9 @@ namespace osu.Game.Screens.Play
{ {
protected const float BACKGROUND_BLUR = 15; protected const float BACKGROUND_BLUR = 15;
private const double content_out_duration = 300; protected const double CONTENT_OUT_DURATION = 300;
protected virtual double PlayerPushDelay => 1800;
public override bool HideOverlaysOnEnter => hideOverlays; public override bool HideOverlaysOnEnter => hideOverlays;
@ -67,6 +70,7 @@ namespace osu.Game.Screens.Play
private readonly BindableDouble volumeAdjustment = new BindableDouble(1); private readonly BindableDouble volumeAdjustment = new BindableDouble(1);
private AudioFilter lowPassFilter; private AudioFilter lowPassFilter;
private AudioFilter highPassFilter;
protected bool BackgroundBrightnessReduction protected bool BackgroundBrightnessReduction
{ {
@ -168,7 +172,8 @@ namespace osu.Game.Screens.Play
}, },
idleTracker = new IdleTracker(750), idleTracker = new IdleTracker(750),
}), }),
lowPassFilter = new AudioFilter(audio.TrackMixer) lowPassFilter = new AudioFilter(audio.TrackMixer),
highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass)
}; };
if (Beatmap.Value.BeatmapInfo.EpilepsyWarning) if (Beatmap.Value.BeatmapInfo.EpilepsyWarning)
@ -210,7 +215,7 @@ namespace osu.Game.Screens.Play
// after an initial delay, start the debounced load check. // after an initial delay, start the debounced load check.
// this will continue to execute even after resuming back on restart. // this will continue to execute even after resuming back on restart.
Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, Clock.CurrentTime + 1800, 0)); Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, Clock.CurrentTime + PlayerPushDelay, 0));
showMuteWarningIfNeeded(); showMuteWarningIfNeeded();
showBatteryWarningIfNeeded(); showBatteryWarningIfNeeded();
@ -239,18 +244,19 @@ namespace osu.Game.Screens.Play
Beatmap.Value.Track.Stop(); Beatmap.Value.Track.Stop();
Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF); lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
highPassFilter.CutoffTo(0);
} }
public override bool OnExiting(IScreen next) public override bool OnExiting(IScreen next)
{ {
cancelLoad(); cancelLoad();
contentOut(); ContentOut();
// If the load sequence was interrupted, the epilepsy warning may already be displayed (or in the process of being displayed). // If the load sequence was interrupted, the epilepsy warning may already be displayed (or in the process of being displayed).
epilepsyWarning?.Hide(); epilepsyWarning?.Hide();
// Ensure the screen doesn't expire until all the outwards fade operations have completed. // Ensure the screen doesn't expire until all the outwards fade operations have completed.
this.Delay(content_out_duration).FadeOut(); this.Delay(CONTENT_OUT_DURATION).FadeOut();
ApplyToBackground(b => b.IgnoreUserSettings.Value = true); ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
@ -266,9 +272,9 @@ namespace osu.Game.Screens.Play
const double duration = 300; const double duration = 300;
if (!resuming) logo.MoveTo(new Vector2(0.5f), duration, Easing.In); if (!resuming) logo.MoveTo(new Vector2(0.5f), duration, Easing.OutQuint);
logo.ScaleTo(new Vector2(0.15f), duration, Easing.In); logo.ScaleTo(new Vector2(0.15f), duration, Easing.OutQuint);
logo.FadeIn(350); logo.FadeIn(350);
Scheduler.AddDelayed(() => Scheduler.AddDelayed(() =>
@ -352,18 +358,20 @@ namespace osu.Game.Screens.Play
content.FadeInFromZero(400); content.FadeInFromZero(400);
content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer); content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer);
lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint); lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint);
highPassFilter.CutoffTo(300).Then().CutoffTo(0, 1250); // 1250 is to line up with the appearance of MetadataInfo (750 delay + 500 fade-in)
ApplyToBackground(b => b?.FadeColour(Color4.White, 800, Easing.OutQuint)); ApplyToBackground(b => b?.FadeColour(Color4.White, 800, Easing.OutQuint));
} }
private void contentOut() protected virtual void ContentOut()
{ {
// Ensure the logo is no longer tracking before we scale the content // Ensure the logo is no longer tracking before we scale the content
content.StopTracking(); content.StopTracking();
content.ScaleTo(0.7f, content_out_duration * 2, Easing.OutQuint); content.ScaleTo(0.7f, CONTENT_OUT_DURATION * 2, Easing.OutQuint);
content.FadeOut(content_out_duration, Easing.OutQuint); content.FadeOut(CONTENT_OUT_DURATION, Easing.OutQuint);
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, content_out_duration); lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, CONTENT_OUT_DURATION);
highPassFilter.CutoffTo(0, CONTENT_OUT_DURATION);
} }
private void pushWhenLoaded() private void pushWhenLoaded()
@ -388,9 +396,9 @@ namespace osu.Game.Screens.Play
// ensure that once we have reached this "point of no return", readyForPush will be false for all future checks (until a new player instance is prepared). // ensure that once we have reached this "point of no return", readyForPush will be false for all future checks (until a new player instance is prepared).
var consumedPlayer = consumePlayer(); var consumedPlayer = consumePlayer();
contentOut(); ContentOut();
TransformSequence<PlayerLoader> pushSequence = this.Delay(content_out_duration); TransformSequence<PlayerLoader> pushSequence = this.Delay(CONTENT_OUT_DURATION);
// only show if the warning was created (i.e. the beatmap needs it) // only show if the warning was created (i.e. the beatmap needs it)
// and this is not a restart of the map (the warning expires after first load). // and this is not a restart of the map (the warning expires after first load).
@ -412,7 +420,7 @@ namespace osu.Game.Screens.Play
else else
{ {
// This goes hand-in-hand with the restoration of low pass filter in contentOut(). // This goes hand-in-hand with the restoration of low pass filter in contentOut().
this.TransformBindableTo(volumeAdjustment, 0, content_out_duration, Easing.OutCubic); this.TransformBindableTo(volumeAdjustment, 0, CONTENT_OUT_DURATION, Easing.OutCubic);
} }
pushSequence.Schedule(() => pushSequence.Schedule(() =>

View File

@ -270,7 +270,7 @@ namespace osu.Game.Screens.Play
colourNormal = colours.Yellow; colourNormal = colours.Yellow;
colourHover = colours.YellowDark; colourHover = colours.YellowDark;
sampleConfirm = audio.Samples.Get(@"SongSelect/confirm-selection"); sampleConfirm = audio.Samples.Get(@"UI/submit-select");
Children = new Drawable[] Children = new Drawable[]
{ {

View File

@ -113,7 +113,7 @@ namespace osu.Game.Screens.Select.Carousel
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}; };
sampleHover = audio.Samples.Get("SongSelect/song-ping"); sampleHover = audio.Samples.Get("UI/default-hover");
} }
public bool InsetForBorder public bool InsetForBorder

View File

@ -105,6 +105,8 @@ namespace osu.Game.Screens.Select
private readonly Bindable<RulesetInfo> decoupledRuleset = new Bindable<RulesetInfo>(); private readonly Bindable<RulesetInfo> decoupledRuleset = new Bindable<RulesetInfo>();
private double audioFeedbackLastPlaybackTime;
[Resolved] [Resolved]
private MusicController music { get; set; } private MusicController music { get; set; }
@ -435,6 +437,7 @@ namespace osu.Game.Screens.Select
} }
// We need to keep track of the last selected beatmap ignoring debounce to play the correct selection sounds. // We need to keep track of the last selected beatmap ignoring debounce to play the correct selection sounds.
private BeatmapInfo beatmapInfoPrevious;
private BeatmapInfo beatmapInfoNoDebounce; private BeatmapInfo beatmapInfoNoDebounce;
private RulesetInfo rulesetNoDebounce; private RulesetInfo rulesetNoDebounce;
@ -477,6 +480,21 @@ namespace osu.Game.Screens.Select
else else
selectionChangedDebounce = Scheduler.AddDelayed(run, 200); selectionChangedDebounce = Scheduler.AddDelayed(run, 200);
if (beatmap != beatmapInfoPrevious)
{
if (beatmap != null && beatmapInfoPrevious != null && Time.Current - audioFeedbackLastPlaybackTime >= 50)
{
if (beatmap.BeatmapSetInfoID == beatmapInfoPrevious.BeatmapSetInfoID)
sampleChangeDifficulty.Play();
else
sampleChangeBeatmap.Play();
audioFeedbackLastPlaybackTime = Time.Current;
}
beatmapInfoPrevious = beatmap;
}
void run() void run()
{ {
// clear pending task immediately to track any potential nested debounce operation. // clear pending task immediately to track any potential nested debounce operation.
@ -508,18 +526,7 @@ namespace osu.Game.Screens.Select
if (!EqualityComparer<BeatmapInfo>.Default.Equals(beatmap, Beatmap.Value.BeatmapInfo)) if (!EqualityComparer<BeatmapInfo>.Default.Equals(beatmap, Beatmap.Value.BeatmapInfo))
{ {
Logger.Log($"beatmap changed from \"{Beatmap.Value.BeatmapInfo}\" to \"{beatmap}\""); Logger.Log($"beatmap changed from \"{Beatmap.Value.BeatmapInfo}\" to \"{beatmap}\"");
int? lastSetID = Beatmap.Value?.BeatmapInfo.BeatmapSetInfoID;
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap);
if (beatmap != null)
{
if (beatmap.BeatmapSetInfoID == lastSetID)
sampleChangeDifficulty.Play();
else
sampleChangeBeatmap.Play();
}
} }
if (this.IsCurrentScreen()) if (this.IsCurrentScreen())

View File

@ -31,5 +31,23 @@ namespace osu.Game.Utils
/// </summary> /// </summary>
/// <param name="rank">The rank/position to be formatted.</param> /// <param name="rank">The rank/position to be formatted.</param>
public static string FormatRank(this int rank) => rank.ToMetric(decimals: rank < 100_000 ? 1 : 0); public static string FormatRank(this int rank) => rank.ToMetric(decimals: rank < 100_000 ? 1 : 0);
/// <summary>
/// Finds the number of digits after the decimal.
/// </summary>
/// <param name="d">The value to find the number of decimal digits for.</param>
/// <returns>The number decimal digits.</returns>
public static int FindPrecision(decimal d)
{
int precision = 0;
while (d != Math.Round(d))
{
d *= 10;
precision++;
}
return precision;
}
} }
} }

View File

@ -9,6 +9,34 @@ namespace osu.Game.Utils
{ {
public static class ZipUtils public static class ZipUtils
{ {
public static bool IsZipArchive(MemoryStream stream)
{
try
{
stream.Seek(0, SeekOrigin.Begin);
using (var arc = ZipArchive.Open(stream))
{
foreach (var entry in arc.Entries)
{
using (entry.OpenEntryStream())
{
}
}
}
return true;
}
catch (Exception)
{
return false;
}
finally
{
stream.Seek(0, SeekOrigin.Begin);
}
}
public static bool IsZipArchive(string path) public static bool IsZipArchive(string path)
{ {
if (!File.Exists(path)) if (!File.Exists(path))

View File

@ -37,7 +37,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.6.0" /> <PackageReference Include="Realm" Version="10.6.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.1108.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.1108.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1026.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.1112.0" />
<PackageReference Include="Sentry" Version="3.10.0" /> <PackageReference Include="Sentry" Version="3.10.0" />
<PackageReference Include="SharpCompress" Version="0.30.0" /> <PackageReference Include="SharpCompress" Version="0.30.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />

View File

@ -71,7 +71,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.1108.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.1108.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1026.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.1112.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
<PropertyGroup> <PropertyGroup>