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

Merge branch 'master' into playlist-item-iruleset

This commit is contained in:
Dan Balasescu 2021-11-16 16:19:10 +09:00 committed by GitHub
commit ddf2fade1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1115 additions and 174 deletions

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" />
</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" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">

View File

@ -10,9 +10,13 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
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 osuTK.Graphics;
using osuTK.Input;
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);
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;
});
}
[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]
@ -111,6 +151,35 @@ namespace osu.Game.Tests.Visual.Editing
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()
{
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 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
{
public new FailOverlay FailOverlay => base.FailOverlay;
public bool FrequencyIncreased { get; private set; }
public FailPlayer()
: base(false, false)
{
@ -47,6 +53,19 @@ namespace osu.Game.Tests.Visual.Gameplay
base.LoadComplete();
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

@ -163,6 +163,8 @@ namespace osu.Game.Beatmaps
return false;
}
public bool Equals(IBeatmapInfo other) => other is BeatmapInfo b && Equals(b);
public bool AudioEquals(BeatmapInfo other) => other != null && BeatmapSet != null && other.BeatmapSet != null &&
BeatmapSet.Hash == other.BeatmapSet.Hash &&
(Metadata ?? BeatmapSet.Metadata).AudioFile == (other.Metadata ?? other.BeatmapSet.Metadata).AudioFile;

View File

@ -78,6 +78,8 @@ namespace osu.Game.Beatmaps
return false;
}
public bool Equals(IBeatmapSetInfo other) => other is BeatmapSetInfo b && Equals(b);
#region Implementation of IHasOnlineID
int IHasOnlineID<int>.OnlineID => OnlineID ?? -1;

View File

@ -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;
using osu.Game.Database;
using osu.Game.Rulesets;
@ -11,7 +12,7 @@ namespace osu.Game.Beatmaps
/// <summary>
/// A single beatmap difficulty.
/// </summary>
public interface IBeatmapInfo : IHasOnlineID<int>
public interface IBeatmapInfo : IHasOnlineID<int>, IEquatable<IBeatmapInfo>
{
/// <summary>
/// The user-specified name given to this beatmap.

View File

@ -12,7 +12,7 @@ namespace osu.Game.Beatmaps
/// <summary>
/// A representation of a collection of beatmap difficulties, generally packaged as an ".osz" archive.
/// </summary>
public interface IBeatmapSetInfo : IHasOnlineID<int>
public interface IBeatmapSetInfo : IHasOnlineID<int>, IEquatable<IBeatmapSetInfo>
{
/// <summary>
/// The date when this beatmap was imported.

View File

@ -17,33 +17,69 @@ namespace osu.Game.Beatmaps
{
public interface IWorkingBeatmap
{
IBeatmapInfo BeatmapInfo { get; }
IBeatmapSetInfo BeatmapSetInfo { get; }
IBeatmapMetadataInfo Metadata { get; }
/// <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>
IBeatmap Beatmap { get; }
/// <summary>
/// Retrieves the background for this <see cref="WorkingBeatmap"/>.
/// Retrieves the background for this <see cref="IWorkingBeatmap"/>.
/// </summary>
Texture Background { get; }
/// <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>
Waveform Waveform { get; }
/// <summary>
/// Retrieves the <see cref="Storyboard"/> which this <see cref="WorkingBeatmap"/> provides.
/// Retrieves the <see cref="Storyboard"/> which this <see cref="IWorkingBeatmap"/> provides.
/// </summary>
Storyboard Storyboard { get; }
/// <summary>
/// Retrieves the <see cref="Skin"/> which this <see cref="WorkingBeatmap"/> provides.
/// Retrieves the <see cref="Skin"/> which this <see cref="IWorkingBeatmap"/> provides.
/// </summary>
ISkin Skin { get; }
/// <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>
Track Track { get; }
@ -67,7 +103,7 @@ namespace osu.Game.Beatmaps
/// </summary>
/// <remarks>
/// 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
/// outside of the game context.
/// </remarks>
@ -79,5 +115,20 @@ namespace osu.Game.Beatmaps
/// </summary>
/// <param name="storagePath">The storage path to the file.</param>
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 readonly BeatmapInfo BeatmapInfo;
public readonly BeatmapSetInfo BeatmapSetInfo;
public readonly BeatmapMetadata Metadata;
protected AudioManager AudioManager { get; }
@ -89,6 +87,9 @@ namespace osu.Game.Beatmaps
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);
// Check if the beatmap can be converted
@ -176,17 +177,8 @@ namespace osu.Game.Beatmaps
private CancellationTokenSource loadCancellation = new CancellationTokenSource();
/// <summary>
/// Beings loading the contents of this <see cref="WorkingBeatmap"/> asynchronously.
/// </summary>
public void BeginAsyncLoad()
{
loadBeatmapAsync();
}
public void BeginAsyncLoad() => loadBeatmapAsync();
/// <summary>
/// Cancels the asynchronous loading of the contents of this <see cref="WorkingBeatmap"/>.
/// </summary>
public void CancelAsyncLoad()
{
lock (beatmapFetchLock)
@ -234,6 +226,10 @@ namespace osu.Game.Beatmaps
public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
IBeatmapInfo IWorkingBeatmap.BeatmapInfo => BeatmapInfo;
IBeatmapMetadataInfo IWorkingBeatmap.Metadata => Metadata;
IBeatmapSetInfo IWorkingBeatmap.BeatmapSetInfo => BeatmapSetInfo;
public IBeatmap Beatmap
{
get
@ -273,9 +269,6 @@ namespace osu.Game.Beatmaps
[NotNull]
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()
{
Track.Looping = true;

View File

@ -47,10 +47,30 @@ namespace osu.Game.Database
/// </summary>
public ArchiveReader GetReader()
{
if (Stream != null)
return new ZipArchiveReader(Stream, Path);
return Stream != null
? 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>

View File

@ -3,6 +3,7 @@
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Users;
@ -62,12 +63,38 @@ namespace osu.Game.Extensions
}
/// <summary>
/// Check whether the online ID of two instances match.
/// Check whether the online ID of two <see cref="IBeatmapSetInfo"/>s match.
/// </summary>
/// <param name="instance">The instance to compare.</param>
/// <param name="other">The other instance to compare against.</param>
/// <returns>Whether online IDs match. If either instance is missing an online ID, this will return false.</returns>
public static bool MatchesOnlineID(this IHasOnlineID<long>? instance, IHasOnlineID<long>? other)
public static bool MatchesOnlineID(this IBeatmapSetInfo? instance, IBeatmapSetInfo? other) => matchesOnlineID(instance, other);
/// <summary>
/// Check whether the online ID of two <see cref="IBeatmapInfo"/>s match.
/// </summary>
/// <param name="instance">The instance to compare.</param>
/// <param name="other">The other instance to compare against.</param>
/// <returns>Whether online IDs match. If either instance is missing an online ID, this will return false.</returns>
public static bool MatchesOnlineID(this IBeatmapInfo? instance, IBeatmapInfo? other) => matchesOnlineID(instance, other);
/// <summary>
/// Check whether the online ID of two <see cref="IRulesetInfo"/>s match.
/// </summary>
/// <param name="instance">The instance to compare.</param>
/// <param name="other">The other instance to compare against.</param>
/// <returns>Whether online IDs match. If either instance is missing an online ID, this will return false.</returns>
public static bool MatchesOnlineID(this IRulesetInfo? instance, IRulesetInfo? other) => matchesOnlineID(instance, other);
/// <summary>
/// Check whether the online ID of two <see cref="APIUser"/>s match.
/// </summary>
/// <param name="instance">The instance to compare.</param>
/// <param name="other">The other instance to compare against.</param>
/// <returns>Whether online IDs match. If either instance is missing an online ID, this will return false.</returns>
public static bool MatchesOnlineID(this APIUser? instance, APIUser? other) => matchesOnlineID(instance, other);
private static bool matchesOnlineID(this IHasOnlineID<long>? instance, IHasOnlineID<long>? other)
{
if (instance == null || other == null)
return false;
@ -78,13 +105,7 @@ namespace osu.Game.Extensions
return instance.OnlineID.Equals(other.OnlineID);
}
/// <summary>
/// Check whether the online ID of two instances match.
/// </summary>
/// <param name="instance">The instance to compare.</param>
/// <param name="other">The other instance to compare against.</param>
/// <returns>Whether online IDs match. If either instance is missing an online ID, this will return false.</returns>
public static bool MatchesOnlineID(this IHasOnlineID<int>? instance, IHasOnlineID<int>? other)
private static bool matchesOnlineID(this IHasOnlineID<int>? instance, IHasOnlineID<int>? other)
{
if (instance == null || other == null)
return false;

View File

@ -52,15 +52,18 @@ namespace osu.Game.Graphics
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.5f, Color4Extensions.FromHex("7cff4f")),
(3.25f, Color4Extensions.FromHex("f6f05c")),
(4.5f, Color4Extensions.FromHex("ff8068")),
(6.0f, Color4Extensions.FromHex("ff3c71")),
(7.0f, Color4Extensions.FromHex("6563de")),
(8.0f, Color4Extensions.FromHex("18158e")),
(8.0f, Color4.Black),
(3.3f, Color4Extensions.FromHex("f6f05c")),
(4.2f, Color4Extensions.FromHex("ff8068")),
(4.9f, Color4Extensions.FromHex("ff4e6f")),
(5.8f, Color4Extensions.FromHex("c645b8")),
(6.7f, Color4Extensions.FromHex("6563de")),
(7.7f, Color4Extensions.FromHex("18158e")),
(9.0f, Color4.Black),
}, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero));
/// <summary>

View File

@ -19,6 +19,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Overlays;
using osu.Game.Utils;
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);
// 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}");
}
@ -248,23 +249,5 @@ namespace osu.Game.Graphics.UserInterface
/// <returns>The normalised decimal.</returns>
private decimal normalise(decimal d, int sd)
=> 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
{
/// <summary>
/// Allows reading a single file from the provided stream.
/// Allows reading a single file from the provided byte array.
/// </summary>
public class LegacyByteArrayReader : ArchiveReader
{

View File

@ -106,6 +106,8 @@ namespace osu.Game.Models
return ID == other.ID;
}
public bool Equals(IBeatmapInfo? other) => other is RealmBeatmap b && Equals(b);
public bool AudioEquals(RealmBeatmap? other) => other != null
&& BeatmapSet != null
&& other.BeatmapSet != null

View File

@ -64,6 +64,8 @@ namespace osu.Game.Models
public override string ToString() => Metadata?.GetDisplayString() ?? base.ToString();
public bool Equals(IBeatmapSetInfo? other) => other is RealmBeatmapSet b && Equals(b);
IEnumerable<IBeatmapInfo> IBeatmapSetInfo.Beatmaps => Beatmaps;
IEnumerable<INamedFileUsage> IBeatmapSetInfo.Files => Files;

View File

@ -4,6 +4,7 @@
using System;
using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Rulesets;
#nullable enable
@ -103,5 +104,7 @@ namespace osu.Game.Online.API.Requests.Responses
public string Hash => throw new NotImplementedException();
#endregion
public bool Equals(IBeatmapInfo? other) => other is APIBeatmap b && this.MatchesOnlineID(b);
}
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Extensions;
#nullable enable
@ -141,5 +142,7 @@ namespace osu.Game.Online.API.Requests.Responses
double IBeatmapSetInfo.MaxBPM => BPM;
#endregion
public bool Equals(IBeatmapSetInfo? other) => other is APIBeatmapSet b && this.MatchesOnlineID(b);
}
}

View File

@ -7,7 +7,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Graphics.Containers;
using osuTK;
@ -30,7 +29,7 @@ namespace osu.Game.Overlays.Music
var items = (SearchContainer<RearrangeableListItem<BeatmapSetInfo>>)ListContainer;
foreach (var item in items.OfType<PlaylistItem>())
item.InSelectedCollection = criteria.Collection?.Beatmaps.Any(b => b.MatchesOnlineID(item.Model)) ?? true;
item.InSelectedCollection = criteria.Collection?.Beatmaps.Any(b => item.Model.Equals(b.BeatmapSet)) ?? true;
items.SearchTerm = criteria.SearchText;
}

View File

@ -6,7 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.Settings
{
@ -29,36 +29,26 @@ namespace osu.Game.Overlays.Settings
Children = new Drawable[]
{
new FillFlowContainer
new OsuTextFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Children = new Drawable[]
Padding = new MarginPadding
{
new OsuSpriteText
{
Text = heading,
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,
Horizontal = SettingsPanel.CONTENT_MARGINS,
Top = Toolbar.Toolbar.TOOLTIP_HEIGHT,
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.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@ -14,19 +16,20 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Timing;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class DifficultyPointPiece : HitObjectPointPiece, IHasPopover
{
private readonly HitObject hitObject;
public readonly HitObject HitObject;
private readonly BindableNumber<double> speedMultiplier;
public DifficultyPointPiece(HitObject hitObject)
: base(hitObject.DifficultyControlPoint)
{
this.hitObject = hitObject;
HitObject = hitObject;
speedMultiplier = hitObject.DifficultyControlPoint.SliderVelocityBindable.GetBoundCopy();
}
@ -44,14 +47,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return true;
}
public Popover GetPopover() => new DifficultyEditPopover(hitObject);
public Popover GetPopover() => new DifficultyEditPopover(HitObject);
public class DifficultyEditPopover : OsuPopover
{
private readonly HitObject hitObject;
private readonly DifficultyControlPoint point;
private SliderWithTextBoxInput<double> sliderVelocitySlider;
private IndeterminateSliderWithTextBoxInput<double> sliderVelocitySlider;
[Resolved(canBeNull: true)]
private EditorBeatmap beatmap { get; set; }
@ -59,7 +61,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public DifficultyEditPopover(HitObject hitObject)
{
this.hitObject = hitObject;
point = hitObject.DifficultyControlPoint;
}
[BackgroundDependencyLoader]
@ -72,11 +73,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Width = 200,
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0, 15),
Children = new Drawable[]
{
sliderVelocitySlider = new SliderWithTextBoxInput<double>("Velocity")
sliderVelocitySlider = new IndeterminateSliderWithTextBoxInput<double>("Velocity", new DifficultyControlPoint().SliderVelocityBindable)
{
Current = new DifficultyControlPoint().SliderVelocityBindable,
KeyboardStep = 0.1f
},
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();
// even if there are multiple objects selected, we can still display a value if they all have the same value.
var selectedPointBindable = relevantControlPoints.Select(point => point.SliderVelocity).Distinct().Count() == 1 ? relevantControlPoints.First().SliderVelocityBindable : null;
if (selectedPointBindable != null)
{
// 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.
double expectedPrecision = new DifficultyControlPoint().SliderVelocityBindable.Precision;
if (selectedPointBindable.Precision < expectedPrecision)
selectedPointBindable.Precision = expectedPrecision;
sliderVelocitySlider.Current.Value = selectedPointBindable.Value;
}
sliderVelocitySlider.Current = selectedPointBindable;
sliderVelocitySlider.Current.BindValueChanged(_ => beatmap?.Update(hitObject));
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.
// 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.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@ -14,12 +19,13 @@ using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Timing;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class SamplePointPiece : HitObjectPointPiece, IHasPopover
{
private readonly HitObject hitObject;
public readonly HitObject HitObject;
private readonly Bindable<string> bank;
private readonly BindableNumber<int> volume;
@ -27,7 +33,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public SamplePointPiece(HitObject hitObject)
: base(hitObject.SampleControlPoint)
{
this.hitObject = hitObject;
HitObject = hitObject;
volume = hitObject.SampleControlPoint.SampleVolumeBindable.GetBoundCopy();
bank = hitObject.SampleControlPoint.SampleBankBindable.GetBoundCopy();
}
@ -50,23 +56,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Label.Text = $"{bank.Value} {volume.Value}";
}
public Popover GetPopover() => new SampleEditPopover(hitObject);
public Popover GetPopover() => new SampleEditPopover(HitObject);
public class SampleEditPopover : OsuPopover
{
private readonly HitObject hitObject;
private readonly SampleControlPoint point;
private LabelledTextBox bank;
private SliderWithTextBoxInput<int> volume;
private LabelledTextBox bank = null!;
private IndeterminateSliderWithTextBoxInput<int> volume = null!;
[Resolved(canBeNull: true)]
private EditorBeatmap beatmap { get; set; }
private EditorBeatmap beatmap { get; set; } = null!;
public SampleEditPopover(HitObject hitObject)
{
this.hitObject = hitObject;
point = hitObject.SampleControlPoint;
}
[BackgroundDependencyLoader]
@ -79,25 +83,84 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Width = 200,
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0, 10),
Children = new Drawable[]
{
bank = new LabelledTextBox
{
Label = "Bank Name",
},
volume = new SliderWithTextBoxInput<int>("Volume")
{
Current = new SampleControlPoint().SampleVolumeBindable,
}
volume = new IndeterminateSliderWithTextBoxInput<int>("Volume", new SampleControlPoint().SampleVolumeBindable)
}
}
};
bank.Current = point.SampleBankBindable;
bank.Current.BindValueChanged(_ => beatmap.Update(hitObject));
// 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.SampleControlPoint).ToArray();
volume.Current = point.SampleVolumeBindable;
volume.Current.BindValueChanged(_ => beatmap.Update(hitObject));
// even if there are multiple objects selected, we can still display sample volume or bank if they all have the same value.
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.Compose;
using osu.Game.Screens.Edit.Design;
using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Screens.Edit.Verify;
@ -324,6 +325,19 @@ namespace osu.Game.Screens.Edit
/// </summary>
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>
/// Restore the editor to a provided state.
/// </summary>
@ -486,7 +500,18 @@ namespace osu.Game.Screens.Edit
public override void OnEntering(IScreen last)
{
base.OnEntering(last);
dimBackground();
resetTrack(true);
}
public override void OnResuming(IScreen last)
{
base.OnResuming(last);
dimBackground();
}
private void dimBackground()
{
ApplyToBackground(b =>
{
// 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.BlurAmount.Value = 0;
});
resetTrack(true);
}
public override bool OnExiting(IScreen next)
@ -535,9 +558,9 @@ namespace osu.Game.Screens.Edit
public override void OnSuspending(IScreen next)
{
refetchBeatmap();
base.OnSuspending(next);
clock.Stop();
refetchBeatmap();
}
private void refetchBeatmap()
@ -770,11 +793,7 @@ namespace osu.Game.Screens.Edit
return new DifficultyMenuItem(beatmapInfo, isCurrentDifficulty, SwitchToDifficulty);
}
protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleDifficultySwitch(nextBeatmap, new EditorState
{
Time = clock.CurrentTimeAccurate,
ClipboardContent = editorBeatmap.BeatmapInfo.RulesetID == nextBeatmap.RulesetID ? Clipboard.Content.Value : string.Empty
});
protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleDifficultySwitch(nextBeatmap, GetState(nextBeatmap));
private void cancelExit()
{
@ -797,7 +816,7 @@ namespace osu.Game.Screens.Edit
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);

View File

@ -3,21 +3,30 @@
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.Edit
namespace osu.Game.Screens.Edit.GameplayTest
{
public class EditorPlayer : Player
{
public EditorPlayer()
: base(new PlayerConfiguration { ShowResults = false })
{
}
private readonly Editor editor;
private readonly EditorState editorState;
[Resolved]
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()
{
base.LoadComplete();
@ -35,9 +44,22 @@ namespace osu.Game.Screens.Edit
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)
{
musicController.Stop();
editorState.Time = GameplayClockContainer.CurrentTime;
editor.RestoreState(editorState);
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.Game.Overlays.Dialog;
namespace osu.Game.Screens.Edit
namespace osu.Game.Screens.Edit.GameplayTest
{
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(_ =>
{
RemoveFilters();
// Don't reset frequency as the pause screen may appear post transform, causing a second frequency sweep.
RemoveFilters(false);
OnComplete?.Invoke();
});
@ -137,15 +138,16 @@ namespace osu.Game.Screens.Play
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)
return;
RemoveInternal(filters);
filters.Dispose();
track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq);
}
protected override void Update()

View File

@ -5,6 +5,7 @@ using System;
using System.Diagnostics;
using System.Threading.Tasks;
using JetBrains.Annotations;
using ManagedBass.Fx;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
@ -35,7 +36,9 @@ namespace osu.Game.Screens.Play
{
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;
@ -67,6 +70,7 @@ namespace osu.Game.Screens.Play
private readonly BindableDouble volumeAdjustment = new BindableDouble(1);
private AudioFilter lowPassFilter;
private AudioFilter highPassFilter;
protected bool BackgroundBrightnessReduction
{
@ -168,7 +172,8 @@ namespace osu.Game.Screens.Play
},
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)
@ -210,7 +215,7 @@ namespace osu.Game.Screens.Play
// after an initial delay, start the debounced load check.
// 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();
showBatteryWarningIfNeeded();
@ -239,18 +244,19 @@ namespace osu.Game.Screens.Play
Beatmap.Value.Track.Stop();
Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
highPassFilter.CutoffTo(0);
}
public override bool OnExiting(IScreen next)
{
cancelLoad();
contentOut();
ContentOut();
// If the load sequence was interrupted, the epilepsy warning may already be displayed (or in the process of being displayed).
epilepsyWarning?.Hide();
// 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);
@ -266,9 +272,9 @@ namespace osu.Game.Screens.Play
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);
Scheduler.AddDelayed(() =>
@ -352,18 +358,20 @@ namespace osu.Game.Screens.Play
content.FadeInFromZero(400);
content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer);
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));
}
private void contentOut()
protected virtual void ContentOut()
{
// Ensure the logo is no longer tracking before we scale the content
content.StopTracking();
content.ScaleTo(0.7f, content_out_duration * 2, Easing.OutQuint);
content.FadeOut(content_out_duration, Easing.OutQuint);
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, content_out_duration);
content.ScaleTo(0.7f, CONTENT_OUT_DURATION * 2, Easing.OutQuint);
content.FadeOut(CONTENT_OUT_DURATION, Easing.OutQuint);
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, CONTENT_OUT_DURATION);
highPassFilter.CutoffTo(0, CONTENT_OUT_DURATION);
}
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).
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)
// 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
{
// 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(() =>

View File

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

View File

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

View File

@ -105,6 +105,8 @@ namespace osu.Game.Screens.Select
private readonly Bindable<RulesetInfo> decoupledRuleset = new Bindable<RulesetInfo>();
private double audioFeedbackLastPlaybackTime;
[Resolved]
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.
private BeatmapInfo beatmapInfoPrevious;
private BeatmapInfo beatmapInfoNoDebounce;
private RulesetInfo rulesetNoDebounce;
@ -477,6 +480,21 @@ namespace osu.Game.Screens.Select
else
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()
{
// 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))
{
Logger.Log($"beatmap changed from \"{Beatmap.Value.BeatmapInfo}\" to \"{beatmap}\"");
int? lastSetID = Beatmap.Value?.BeatmapInfo.BeatmapSetInfoID;
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap);
if (beatmap != null)
{
if (beatmap.BeatmapSetInfoID == lastSetID)
sampleChangeDifficulty.Play();
else
sampleChangeBeatmap.Play();
}
}
if (this.IsCurrentScreen())

View File

@ -31,5 +31,23 @@ namespace osu.Game.Utils
/// </summary>
/// <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);
/// <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 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)
{
if (!File.Exists(path))

View File

@ -37,7 +37,7 @@
</PackageReference>
<PackageReference Include="Realm" Version="10.6.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="SharpCompress" Version="0.30.0" />
<PackageReference Include="NUnit" Version="3.13.2" />

View File

@ -71,7 +71,7 @@
</ItemGroup>
<ItemGroup Label="Package References">
<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>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
<PropertyGroup>