1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-08 06:22:55 +08:00

Merge branch 'master' into friend-notification-samples

This commit is contained in:
Dean Herbert 2025-01-29 17:43:56 +09:00
commit 9db8e0b7ac
No known key found for this signature in database
15 changed files with 342 additions and 63 deletions

View File

@ -14,6 +14,7 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.Edit.Setup;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
@ -228,7 +229,7 @@ namespace osu.Game.Rulesets.Catch
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
[
new MetadataSection(),
new DifficultySection(),
new CatchDifficultySection(),
new FillFlowContainer
{
AutoSizeAxes = Axes.Y,

View File

@ -0,0 +1,125 @@
// 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.Graphics;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Catch.Edit.Setup
{
public partial class CatchDifficultySection : SetupSection
{
private FormSliderBar<float> circleSizeSlider { get; set; } = null!;
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
private FormSliderBar<float> approachRateSlider { get; set; } = null!;
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
circleSizeSlider = new FormSliderBar<float>
{
Caption = BeatmapsetsStrings.ShowStatsCs,
HintText = EditorSetupStrings.CircleSizeDescription,
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
healthDrainSlider = new FormSliderBar<float>
{
Caption = BeatmapsetsStrings.ShowStatsDrain,
HintText = EditorSetupStrings.DrainRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
approachRateSlider = new FormSliderBar<float>
{
Caption = BeatmapsetsStrings.ShowStatsAr,
HintText = EditorSetupStrings.ApproachRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.ApproachRate)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
baseVelocitySlider = new FormSliderBar<double>
{
Caption = EditorSetupStrings.BaseVelocity,
HintText = EditorSetupStrings.BaseVelocityDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
{
Default = 1.4,
MinValue = 0.4,
MaxValue = 3.6,
Precision = 0.01f,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
tickRateSlider = new FormSliderBar<double>
{
Caption = EditorSetupStrings.TickRate,
HintText = EditorSetupStrings.TickRateDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
{
Default = 1,
MinValue = 1,
MaxValue = 4,
Precision = 1,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
};
foreach (var item in Children.OfType<FormSliderBar<float>>())
item.Current.ValueChanged += _ => updateValues();
foreach (var item in Children.OfType<FormSliderBar<double>>())
item.Current.ValueChanged += _ => updateValues();
}
private void updateValues()
{
// for now, update these on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction.
Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value;
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value;
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
Beatmap.UpdateAllHitObjects();
Beatmap.SaveState();
}
}
}

View File

@ -131,7 +131,7 @@ namespace osu.Game.Tests.Visual.SongSelect
protected void SortBy(FilterCriteria criteria) => AddStep($"sort by {criteria.Sort}", () => Carousel.Filter(criteria));
protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType<BeatmapCarouselPanel>().Count(), () => Is.GreaterThan(0));
protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType<ICarouselPanel>().Count(), () => Is.GreaterThan(0));
protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False);
protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target));

View File

@ -56,16 +56,16 @@ namespace osu.Game.Tests.Visual.SongSelect
WaitForDrawablePanels();
AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2));
AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType<BeatmapCarouselPanel>().Single(p => p.Selected.Value)));
AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value)));
WaitForScrolling();
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapCarouselPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
RemoveFirstBeatmap();
WaitForSorting();
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapCarouselPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
() => Is.EqualTo(positionBefore));
}
@ -83,11 +83,11 @@ namespace osu.Game.Tests.Visual.SongSelect
WaitForScrolling();
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapCarouselPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
RemoveFirstBeatmap();
WaitForSorting();
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapCarouselPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
() => Is.EqualTo(positionBefore));
}

View File

@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
BeatmapCarouselPanel? getSelectedPanel() => Carousel.ChildrenOfType<BeatmapCarouselPanel>().SingleOrDefault(p => p.Selected.Value);
BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType<BeatmapPanel>().SingleOrDefault(p => p.Selected.Value);
}
[Test]

View File

@ -292,7 +292,7 @@ namespace osu.Game.Beatmaps.Drawables
"1407228 II-L - VANGUARD-1.osz",
"1422686 II-L - VANGUARD-2.osz",
"1429217 Street - Phi.osz",
"1442235 2ToneDisco x Cosmicosmo - Shoelaces (feat. Puniden).osz",
"1442235 2ToneDisco x Cosmicosmo - Shoelaces (feat. Puniden).osz", // set is not marked as FA, but track is listed in https://osu.ppy.sh/beatmaps/artists/157
"1447478 Cres. - End Time.osz",
"1449942 m108 - Crescent Sakura.osz",
"1463778 MuryokuP - A tree without a branch.osz",
@ -336,8 +336,8 @@ namespace osu.Game.Beatmaps.Drawables
"1854710 Blaster & Extra Terra - Spacecraft (Cut Ver.).osz",
"1859322 Hino Isuka - Delightness Brightness.osz",
"1884102 Maduk - Go (feat. Lachi) (Cut Ver.).osz",
"1884578 Neko Hacker - People People feat. Nanahira.osz",
"1897902 uma vs. Morimori Atsushi - Re: End of a Dream.osz",
"1884578 Neko Hacker - People People feat. Nanahira.osz", // set is not marked as FA, but track is listed in https://osu.ppy.sh/beatmaps/artists/266
"1897902 uma vs. Morimori Atsushi - Re: End of a Dream.osz", // set is not marked as FA, but track is listed in https://osu.ppy.sh/beatmaps/artists/108
"1905582 KINEMA106 - Fly Away (Cut Ver.).osz",
"1934686 ARForest - Rainbow Magic!!.osz",
"1963076 METAROOM - S.N.U.F.F.Y.osz",
@ -345,7 +345,6 @@ namespace osu.Game.Beatmaps.Drawables
"1971951 James Landino - Shiba Paradise.osz",
"1972518 Toromaru - Sleight of Hand.osz",
"1982302 KINEMA106 - INVITE.osz",
"1983475 KNOWER - The Government Knows.osz",
"2010165 Junk - Yellow Smile (bms edit).osz",
"2022737 Andora - Euphoria (feat. WaMi).osz",
"2025023 tephe - Genjitsu Escape.osz",

View File

@ -9,6 +9,7 @@ using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Toolkit.HighPerformance;
using osu.Framework.Extensions;
using osu.Framework.IO.Stores;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
@ -54,12 +55,22 @@ namespace osu.Game.IO.Archives
if (entry == null)
return null;
var owner = MemoryAllocator.Default.Allocate<byte>((int)entry.Size);
using (Stream s = entry.OpenEntryStream())
s.ReadExactly(owner.Memory.Span);
{
if (entry.Size > 0)
{
var owner = MemoryAllocator.Default.Allocate<byte>((int)entry.Size);
s.ReadExactly(owner.Memory.Span);
return new MemoryOwnerMemoryStream(owner);
}
return new MemoryOwnerMemoryStream(owner);
// due to a sharpcompress bug (https://github.com/adamhathcock/sharpcompress/issues/88),
// in rare instances the `ZipArchiveEntry` will not contain a correct `Size` but instead report 0.
// this would lead to the block above reading nothing, and the game basically seeing an archive full of empty files.
// since the bug is years old now, and this is a rather rare situation anyways (reported once in years),
// work around this locally by falling back to reading as many bytes as possible and using a standard non-pooled memory stream.
return new MemoryStream(s.ReadAllRemainingBytesToArray());
}
}
public override void Dispose()

View File

@ -35,7 +35,8 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.LightenDuringBreaks,
Current = config.GetBindable<bool>(OsuSetting.LightenDuringBreaks)
Current = config.GetBindable<bool>(OsuSetting.LightenDuringBreaks),
Keywords = new[] { "dim", "level" }
},
new SettingsCheckbox
{

View File

@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Localisation;
@ -97,7 +98,17 @@ namespace osu.Game.Screens.Edit.Setup
if (!source.Exists)
return false;
var tagSource = TagLib.File.Create(source.FullName);
TagLib.File? tagSource;
try
{
tagSource = TagLib.File.Create(source.FullName);
}
catch (Exception e)
{
Logger.Error(e, "The selected audio track appears to be corrupted. Please select another one.");
return false;
}
changeResource(source, applyToAllDifficulties, @"audio",
metadata => metadata.AudioFile,
@ -192,16 +203,40 @@ namespace osu.Game.Screens.Edit.Setup
editor?.Save();
}
// to avoid scaring users, both background & audio choosers use fake `FileInfo`s with user-friendly filenames
// when displaying an imported beatmap rather than the actual SHA-named file in storage.
// however, that means that when a background or audio file is chosen that is broken or doesn't exist on disk when switching away from the fake files,
// the rollback could enter an infinite loop, because the fake `FileInfo`s *also* don't exist on disk - at least not in the fake location they indicate.
// to circumvent this issue, just allow rollback to proceed always without actually running any of the change logic to ensure visual consistency.
// note that this means that `Change{BackgroundImage,AudioTrack}()` are required to not have made any modifications to the beatmap files
// (or at least cleaned them up properly themselves) if they return `false`.
private bool rollingBackBackgroundChange;
private bool rollingBackAudioChange;
private void backgroundChanged(ValueChangedEvent<FileInfo?> file)
{
if (rollingBackBackgroundChange)
return;
if (file.NewValue == null || !ChangeBackgroundImage(file.NewValue, backgroundChooser.ApplyToAllDifficulties.Value))
{
rollingBackBackgroundChange = true;
backgroundChooser.Current.Value = file.OldValue;
rollingBackBackgroundChange = false;
}
}
private void audioTrackChanged(ValueChangedEvent<FileInfo?> file)
{
if (rollingBackAudioChange)
return;
if (file.NewValue == null || !ChangeAudioTrack(file.NewValue, audioTrackChooser.ApplyToAllDifficulties.Value))
{
rollingBackAudioChange = true;
audioTrackChooser.Current.Value = file.OldValue;
rollingBackAudioChange = false;
}
}
}
}

View File

@ -141,14 +141,28 @@ namespace osu.Game.Screens.SelectV2
#region Drawable pooling
private readonly DrawablePool<BeatmapCarouselPanel> carouselPanelPool = new DrawablePool<BeatmapCarouselPanel>(100);
private readonly DrawablePool<BeatmapPanel> beatmapPanelPool = new DrawablePool<BeatmapPanel>(100);
private readonly DrawablePool<BeatmapSetPanel> setPanelPool = new DrawablePool<BeatmapSetPanel>(100);
private void setupPools()
{
AddInternal(carouselPanelPool);
AddInternal(beatmapPanelPool);
AddInternal(setPanelPool);
}
protected override Drawable GetDrawableForDisplay(CarouselItem item) => carouselPanelPool.Get();
protected override Drawable GetDrawableForDisplay(CarouselItem item)
{
switch (item.Model)
{
case BeatmapInfo:
return beatmapPanelPool.Get();
case BeatmapSetInfo:
return setPanelPool.Get();
}
throw new InvalidOperationException();
}
#endregion
}

View File

@ -57,7 +57,7 @@ namespace osu.Game.Screens.SelectV2
{
newItems.Add(new CarouselItem(b.BeatmapSet!)
{
DrawHeight = 80,
DrawHeight = BeatmapSetPanel.HEIGHT,
IsGroupSelectionTarget = true
});
}

View File

@ -16,22 +16,25 @@ using osuTK.Graphics;
namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapCarouselPanel : PoolableDrawable, ICarouselPanel
public partial class BeatmapPanel : PoolableDrawable, ICarouselPanel
{
[Resolved]
private BeatmapCarousel carousel { get; set; } = null!;
private Box activationFlash = null!;
private Box background = null!;
private OsuSpriteText text = null!;
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(500, CarouselItem.DEFAULT_HEIGHT);
Masking = true;
InternalChildren = new Drawable[]
{
background = new Box
new Box
{
Colour = Color4.Aqua.Darken(5),
Alpha = 0.8f,
RelativeSizeAxes = Axes.Both,
},
@ -69,63 +72,40 @@ namespace osu.Game.Screens.SelectV2
});
}
protected override void FreeAfterUse()
{
base.FreeAfterUse();
Item = null;
Selected.Value = false;
KeyboardSelected.Value = false;
}
protected override void PrepareForUse()
{
base.PrepareForUse();
Debug.Assert(Item != null);
var beatmap = (BeatmapInfo)Item.Model;
DrawYPosition = Item.CarouselYPosition;
Size = new Vector2(500, Item.DrawHeight);
Masking = true;
background.Colour = (Item.Model is BeatmapInfo ? Color4.Aqua : Color4.Yellow).Darken(5);
text.Text = getTextFor(Item.Model);
text.Text = $"Difficulty: {beatmap.DifficultyName} ({beatmap.StarRating:N1}*)";
this.FadeInFromZero(500, Easing.OutQuint);
}
private string getTextFor(object item)
{
switch (item)
{
case BeatmapInfo bi:
return $"Difficulty: {bi.DifficultyName} ({bi.StarRating:N1}*)";
case BeatmapSetInfo si:
return $"{si.Metadata}";
}
return "unknown";
}
protected override bool OnClick(ClickEvent e)
{
if (carousel.CurrentSelection == Item!.Model)
carousel.TryActivateSelection();
else
if (carousel.CurrentSelection != Item!.Model)
{
carousel.CurrentSelection = Item!.Model;
return true;
}
carousel.TryActivateSelection();
return true;
}
#region ICarouselPanel
public CarouselItem? Item { get; set; }
public BindableBool Selected { get; } = new BindableBool();
public BindableBool KeyboardSelected { get; } = new BindableBool();
public double DrawYPosition { get; set; }
public void Activated()
{
activationFlash.FadeOutFromOne(500, Easing.OutQuint);
}
public void Activated() => activationFlash.FadeOutFromOne(500, Easing.OutQuint);
#endregion
}
}

View File

@ -0,0 +1,101 @@
// 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.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapSetPanel : PoolableDrawable, ICarouselPanel
{
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 2;
[Resolved]
private BeatmapCarousel carousel { get; set; } = null!;
private OsuSpriteText text = null!;
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(500, HEIGHT);
Masking = true;
InternalChildren = new Drawable[]
{
new Box
{
Colour = Color4.Yellow.Darken(5),
Alpha = 0.8f,
RelativeSizeAxes = Axes.Both,
},
text = new OsuSpriteText
{
Padding = new MarginPadding(5),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}
};
KeyboardSelected.BindValueChanged(value =>
{
if (value.NewValue)
{
BorderThickness = 5;
BorderColour = Color4.Pink;
}
else
{
BorderThickness = 0;
}
});
}
protected override void PrepareForUse()
{
base.PrepareForUse();
Debug.Assert(Item != null);
Debug.Assert(Item.IsGroupSelectionTarget);
var beatmapSetInfo = (BeatmapSetInfo)Item.Model;
text.Text = $"{beatmapSetInfo.Metadata}";
this.FadeInFromZero(500, Easing.OutQuint);
}
protected override bool OnClick(ClickEvent e)
{
carousel.CurrentSelection = Item!.Model;
return true;
}
#region ICarouselPanel
public CarouselItem? Item { get; set; }
public BindableBool Selected { get; } = new BindableBool();
public BindableBool KeyboardSelected { get; } = new BindableBool();
public double DrawYPosition { get; set; }
public void Activated()
{
// sets should never be activated.
throw new InvalidOperationException();
}
#endregion
}
}

View File

@ -540,11 +540,13 @@ namespace osu.Game.Screens.SelectV2
{
var c = (ICarouselPanel)panel;
// panel in the process of expiring, ignore it.
if (c.Item == null)
continue;
if (panel.Depth != c.DrawYPosition)
scroll.Panels.ChangeChildDepth(panel, (float)c.DrawYPosition);
Debug.Assert(c.Item != null);
if (c.DrawYPosition != c.Item.CarouselYPosition)
c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed);
@ -631,7 +633,9 @@ namespace osu.Game.Screens.SelectV2
if (drawable is not ICarouselPanel carouselPanel)
throw new InvalidOperationException($"Carousel panel drawables must implement {typeof(ICarouselPanel)}");
carouselPanel.DrawYPosition = item.CarouselYPosition;
carouselPanel.Item = item;
scroll.Add(drawable);
}
@ -650,6 +654,12 @@ namespace osu.Game.Screens.SelectV2
{
panel.FinishTransforms();
panel.Expire();
var carouselPanel = (ICarouselPanel)panel;
carouselPanel.Item = null;
carouselPanel.Selected.Value = false;
carouselPanel.KeyboardSelected.Value = false;
}
#endregion

View File

@ -153,6 +153,8 @@
<string>Editor</string>
</dict>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationCategoryType</key>
<string>public.app-category.music-games</string>
<key>LSSupportsOpeningDocumentsInPlace</key>