1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-14 00:53:19 +08:00

Merge branch 'master' into optimise-search

This commit is contained in:
Bartłomiej Dach 2023-03-07 22:07:29 +01:00 committed by GitHub
commit 6c5ccdfd90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 436 additions and 174 deletions

View File

@ -3,15 +3,53 @@
#
# https://github.com/ppy/osu-framework/wiki/Testing-local-framework-checkout-with-other-projects
$CSPROJ="osu.Game/osu.Game.csproj"
$GAME_CSPROJ="osu.Game/osu.Game.csproj"
$ANDROID_PROPS="osu.Android.props"
$IOS_PROPS="osu.iOS.props"
$SLN="osu.sln"
dotnet remove $CSPROJ package ppy.osu.Framework;
dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj ../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj;
dotnet add $CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj
dotnet remove $GAME_CSPROJ reference ppy.osu.Framework;
dotnet remove $ANDROID_PROPS reference ppy.osu.Framework.Android;
dotnet remove $IOS_PROPS reference ppy.osu.Framework.iOS;
dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj `
../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj `
../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj `
../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj;
dotnet add $GAME_CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj;
dotnet add $ANDROID_PROPS reference ../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj;
dotnet add $IOS_PROPS reference ../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj;
# workaround for dotnet add not inserting $(MSBuildThisFileDirectory) on props files
(Get-Content "osu.Android.props") -replace "`"..\\osu-framework", "`"`$(MSBuildThisFileDirectory)..\osu-framework" | Set-Content "osu.Android.props"
(Get-Content "osu.iOS.props") -replace "`"..\\osu-framework", "`"`$(MSBuildThisFileDirectory)..\osu-framework" | Set-Content "osu.iOS.props"
# needed because iOS framework nupkg includes a set of properties to work around certain issues during building,
# and those get ignored when referencing framework via project, threfore we have to manually include it via props reference.
(Get-Content "osu.iOS.props") |
Foreach-Object {
if ($_ -match "</Project>")
{
" <Import Project=`"`$(MSBuildThisFileDirectory)../osu-framework/osu.Framework.iOS.props`"/>"
}
$_
} | Set-Content "osu.iOS.props"
$TMP=New-TemporaryFile
$SLNF=Get-Content "osu.Desktop.slnf" | ConvertFrom-Json
$TMP=New-TemporaryFile
$SLNF.solution.projects += ("../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj")
ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8
Move-Item -Path $TMP -Destination "osu.Desktop.slnf" -Force
$SLNF=Get-Content "osu.Android.slnf" | ConvertFrom-Json
$SLNF.solution.projects += ("../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj")
ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8
Move-Item -Path $TMP -Destination "osu.Android.slnf" -Force
$SLNF=Get-Content "osu.iOS.slnf" | ConvertFrom-Json
$SLNF.solution.projects += ("../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj")
ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8
Move-Item -Path $TMP -Destination "osu.iOS.slnf" -Force

View File

@ -5,14 +5,41 @@
#
# https://github.com/ppy/osu-framework/wiki/Testing-local-framework-checkout-with-other-projects
CSPROJ="osu.Game/osu.Game.csproj"
GAME_CSPROJ="osu.Game/osu.Game.csproj"
ANDROID_PROPS="osu.Android.props"
IOS_PROPS="osu.iOS.props"
SLN="osu.sln"
dotnet remove $CSPROJ package ppy.osu.Framework
dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj ../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj
dotnet add $CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj
dotnet remove $GAME_CSPROJ reference ppy.osu.Framework
dotnet remove $ANDROID_PROPS reference ppy.osu.Framework.Android
dotnet remove $IOS_PROPS reference ppy.osu.Framework.iOS
dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj \
../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj \
../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj \
../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj
dotnet add $GAME_CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj
dotnet add $ANDROID_PROPS reference ../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj
dotnet add $IOS_PROPS reference ../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj
# workaround for dotnet add not inserting $(MSBuildThisFileDirectory) on props files
sed -i.bak 's:"..\\osu-framework:"$(MSBuildThisFileDirectory)..\\osu-framework:g' ./osu.Android.props && rm osu.Android.props.bak
sed -i.bak 's:"..\\osu-framework:"$(MSBuildThisFileDirectory)..\\osu-framework:g' ./osu.iOS.props && rm osu.iOS.props.bak
# needed because iOS framework nupkg includes a set of properties to work around certain issues during building,
# and those get ignored when referencing framework via project, threfore we have to manually include it via props reference.
sed -i.bak '/<\/Project>/i\
<Import Project=\"$(MSBuildThisFileDirectory)../osu-framework/osu.Framework.iOS.props\"/>\
' ./osu.iOS.props && rm osu.iOS.props.bak
SLNF="osu.Desktop.slnf"
tmp=$(mktemp)
jq '.solution.projects += ["../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj"]' osu.Desktop.slnf > $tmp
mv -f $tmp $SLNF
mv -f $tmp osu.Desktop.slnf
jq '.solution.projects += ["../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj"]' osu.Android.slnf > $tmp
mv -f $tmp osu.Android.slnf
jq '.solution.projects += ["../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj"]' osu.iOS.slnf > $tmp
mv -f $tmp osu.iOS.slnf

View File

@ -8,10 +8,14 @@
<!-- NullabilityInfoContextSupport is disabled by default for Android -->
<NullabilityInfoContextSupport>true</NullabilityInfoContextSupport>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.228.0" />
</ItemGroup>
<ItemGroup>
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
Since Realm objects are not declared directly in Android projects, simply disable Fody. -->

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mailto" />
</intent>
</queries>
</manifest>

View File

@ -167,8 +167,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
if (bodySprite != null)
{
bodySprite.Origin = Anchor.BottomCentre;
bodySprite.Scale = new Vector2(bodySprite.Scale.X, Math.Abs(bodySprite.Scale.Y) * -1);
bodySprite.Origin = Anchor.TopCentre;
bodySprite.Anchor = Anchor.BottomCentre; // needs to be flipped due to scale flip in Update.
}
if (light != null)
@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (bodySprite != null)
{
bodySprite.Origin = Anchor.TopCentre;
bodySprite.Scale = new Vector2(bodySprite.Scale.X, Math.Abs(bodySprite.Scale.Y));
bodySprite.Anchor = Anchor.TopCentre;
}
if (light != null)
@ -211,11 +211,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
base.Update();
missFadeTime.Value ??= holdNote.HoldBrokenTime;
int scaleDirection = (direction.Value == ScrollingDirection.Down ? 1 : -1);
// here we go...
switch (bodyStyle)
{
case LegacyNoteBodyStyle.Stretch:
// this is how lazer works by default. nothing required.
if (bodySprite != null)
bodySprite.Scale = new Vector2(1, scaleDirection);
break;
default:
@ -228,7 +232,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
bodySprite.FillMode = FillMode.Stretch;
// i dunno this looks about right??
bodySprite.Scale = new Vector2(1, 32800 / sprite.DrawHeight);
bodySprite.Scale = new Vector2(1, scaleDirection * 32800 / sprite.DrawHeight);
}
break;

View File

@ -48,7 +48,9 @@ namespace osu.Game.Tests.Skins
// Covers BPM counter.
"Archives/modified-default-20221205.osk",
// Covers judgement counter.
"Archives/modified-default-20230117.osk"
"Archives/modified-default-20230117.osk",
// Covers player avatar and flag.
"Archives/modified-argon-20230305.osk",
};
/// <summary>

View File

@ -15,6 +15,7 @@ using osu.Game.Overlays.Settings;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Tests.Beatmaps.IO;
@ -188,6 +189,33 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("mod overlay closed", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden);
}
[Test]
public void TestChangeToNonSkinnableScreen()
{
advanceToSongSelect();
openSkinEditor();
AddAssert("blueprint container present", () => skinEditor.ChildrenOfType<SkinBlueprintContainer>().Count(), () => Is.EqualTo(1));
AddAssert("placeholder not present", () => skinEditor.ChildrenOfType<NonSkinnableScreenPlaceholder>().Count(), () => Is.Zero);
AddAssert("editor sidebars not empty", () => skinEditor.ChildrenOfType<EditorSidebar>().SelectMany(sidebar => sidebar.Children).Count(), () => Is.GreaterThan(0));
AddStep("add skinnable component", () =>
{
skinEditor.ChildrenOfType<SkinComponentToolbox.ToolboxComponentButton>().First().TriggerClick();
});
AddUntilStep("newly added component selected", () => skinEditor.SelectedComponents, () => Has.Count.EqualTo(1));
AddStep("exit to main menu", () => Game.ScreenStack.CurrentScreen.Exit());
AddAssert("selection cleared", () => skinEditor.SelectedComponents, () => Has.Count.Zero);
AddAssert("blueprint container not present", () => skinEditor.ChildrenOfType<SkinBlueprintContainer>().Count(), () => Is.Zero);
AddAssert("placeholder present", () => skinEditor.ChildrenOfType<NonSkinnableScreenPlaceholder>().Count(), () => Is.EqualTo(1));
AddAssert("editor sidebars empty", () => skinEditor.ChildrenOfType<EditorSidebar>().SelectMany(sidebar => sidebar.Children).Count(), () => Is.Zero);
advanceToSongSelect();
AddAssert("blueprint container present", () => skinEditor.ChildrenOfType<SkinBlueprintContainer>().Count(), () => Is.EqualTo(1));
AddAssert("placeholder not present", () => skinEditor.ChildrenOfType<NonSkinnableScreenPlaceholder>().Count(), () => Is.Zero);
AddAssert("editor sidebars not empty", () => skinEditor.ChildrenOfType<EditorSidebar>().SelectMany(sidebar => sidebar.Children).Count(), () => Is.GreaterThan(0));
}
private void advanceToSongSelect()
{
PushAndConfirm(() => songSelect = new TestPlaySongSelect());

View File

@ -516,15 +516,20 @@ namespace osu.Game.Tests.Visual.SongSelect
{
sets.Clear();
for (int i = 0; i < 20; i++)
for (int i = 0; i < 10; i++)
{
var set = TestResources.CreateTestBeatmapSetInfo(5);
if (i >= 2 && i < 10)
// A total of 6 sets have date submitted (4 don't)
// A total of 5 sets have artist string (3 of which also have date submitted)
if (i >= 2 && i < 8) // i = 2, 3, 4, 5, 6, 7 have submitted date
set.DateSubmitted = DateTimeOffset.Now.AddMinutes(i);
if (i < 5)
if (i < 5) // i = 0, 1, 2, 3, 4 have matching string
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string);
set.Beatmaps.ForEach(b => b.Metadata.Title = $"submitted: {set.DateSubmitted}");
sets.Add(set);
}
});
@ -532,15 +537,26 @@ namespace osu.Game.Tests.Visual.SongSelect
loadBeatmaps(sets);
AddStep("Sort by date submitted", () => carousel.Filter(new FilterCriteria { Sort = SortMode.DateSubmitted }, false));
checkVisibleItemCount(diff: false, count: 8);
checkVisibleItemCount(diff: false, count: 10);
checkVisibleItemCount(diff: true, count: 5);
AddAssert("missing date are at end",
() => carousel.Items.OfType<DrawableCarouselBeatmapSet>().Reverse().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted == null).Count(), () => Is.EqualTo(4));
AddAssert("rest are at start", () => carousel.Items.OfType<DrawableCarouselBeatmapSet>().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted != null).Count(),
() => Is.EqualTo(6));
AddStep("Sort by date submitted and string", () => carousel.Filter(new FilterCriteria
{
Sort = SortMode.DateSubmitted,
SearchText = zzz_string
}, false));
checkVisibleItemCount(diff: false, count: 3);
checkVisibleItemCount(diff: false, count: 5);
checkVisibleItemCount(diff: true, count: 5);
AddAssert("missing date are at end",
() => carousel.Items.OfType<DrawableCarouselBeatmapSet>().Reverse().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted == null).Count(), () => Is.EqualTo(2));
AddAssert("rest are at start", () => carousel.Items.OfType<DrawableCarouselBeatmapSet>().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted != null).Count(),
() => Is.EqualTo(3));
}
[Test]
@ -1129,7 +1145,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
// until step required as we are querying against alive items, which are loaded asynchronously inside DrawableCarouselBeatmapSet.
AddUntilStep($"{count} {(diff ? "diffs" : "sets")} visible", () =>
carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible) == count);
carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible), () => Is.EqualTo(count));
}
private void checkSelectionIsCentered()
@ -1190,8 +1206,11 @@ namespace osu.Game.Tests.Visual.SongSelect
{
get
{
foreach (var item in Scroll.Children)
foreach (var item in Scroll.Children.OrderBy(c => c.Y))
{
if (item.Item?.Visible != true)
continue;
yield return item;
if (item is DrawableCarouselBeatmapSet set)

View File

@ -8,12 +8,12 @@ using osu.Game.Overlays.Dialog;
namespace osu.Game.Collections
{
public partial class DeleteCollectionDialog : DeleteConfirmationDialog
public partial class DeleteCollectionDialog : DangerousActionDialog
{
public DeleteCollectionDialog(Live<BeatmapCollection> collection, Action deleteAction)
{
BodyText = collection.PerformRead(c => $"{c.Name} ({"beatmap".ToQuantity(c.BeatmapMD5Hashes.Count)})");
DeleteAction = deleteAction;
DangerousAction = deleteAction;
}
}
}

View File

@ -39,6 +39,16 @@ namespace osu.Game.Localisation.SkinComponents
/// </summary>
public static LocalisableString TextElementTextDescription => new TranslatableString(getKey(@"text_element_text_description"), "The text to be displayed.");
/// <summary>
/// "Corner radius"
/// </summary>
public static LocalisableString CornerRadius => new TranslatableString(getKey(@"corner_radius"), "Corner radius");
/// <summary>
/// "How rounded the corners should be."
/// </summary>
public static LocalisableString CornerRadiusDescription => new TranslatableString(getKey(@"corner_radius_description"), "How rounded the corners should be.");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -42,7 +42,12 @@ namespace osu.Game.Localisation
/// <summary>
/// "Currently editing"
/// </summary>
public static LocalisableString CurrentlyEditing => new TranslatableString(getKey(@"currently_editing"), "Currently editing");
public static LocalisableString CurrentlyEditing => new TranslatableString(getKey(@"currently_editing"), @"Currently editing");
/// <summary>
/// "All layout elements for layers in the current screen will be reset to defaults."
/// </summary>
public static LocalisableString RevertToDefaultDescription => new TranslatableString(getKey(@"revert_to_default_description"), @"All layout elements for layers in the current screen will be reset to defaults.");
private static string getKey(string key) => $@"{prefix}:{key}";
}

View File

@ -8,18 +8,22 @@ using osu.Game.Localisation;
namespace osu.Game.Overlays.Dialog
{
/// <summary>
/// Base class for various confirmation dialogs that concern deletion actions.
/// A dialog which provides confirmation for actions which result in permanent consequences.
/// Differs from <see cref="ConfirmDialog"/> in that the confirmation button is a "dangerous" one
/// (requires the confirm button to be held).
/// </summary>
public abstract partial class DeleteConfirmationDialog : PopupDialog
/// <remarks>
/// The default implementation comes with text for a generic deletion operation.
/// This can be further customised by specifying custom <see cref="PopupDialog.HeaderText"/>.
/// </remarks>
public abstract partial class DangerousActionDialog : PopupDialog
{
/// <summary>
/// The action which performs the deletion.
/// </summary>
protected Action? DeleteAction { get; set; }
protected Action? DangerousAction { get; set; }
protected DeleteConfirmationDialog()
protected DangerousActionDialog()
{
HeaderText = DeleteConfirmationDialogStrings.HeaderText;
@ -30,7 +34,7 @@ namespace osu.Game.Overlays.Dialog
new PopupDialogDangerousButton
{
Text = DeleteConfirmationDialogStrings.Confirm,
Action = () => DeleteAction?.Invoke()
Action = () => DangerousAction?.Invoke()
},
new PopupDialogCancelButton
{

View File

@ -7,12 +7,12 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Overlays.Mods
{
public partial class DeleteModPresetDialog : DeleteConfirmationDialog
public partial class DeleteModPresetDialog : DangerousActionDialog
{
public DeleteModPresetDialog(Live<ModPreset> modPreset)
{
BodyText = modPreset.PerformRead(preset => preset.Name);
DeleteAction = () => modPreset.PerformWrite(preset => preset.DeletePending = true);
DangerousAction = () => modPreset.PerformWrite(preset => preset.DeletePending = true);
}
}
}

View File

@ -6,12 +6,12 @@ using osu.Game.Overlays.Dialog;
namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
public partial class MassDeleteConfirmationDialog : DeleteConfirmationDialog
public partial class MassDeleteConfirmationDialog : DangerousActionDialog
{
public MassDeleteConfirmationDialog(Action deleteAction)
{
BodyText = "Everything?";
DeleteAction = deleteAction;
DangerousAction = deleteAction;
}
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings
{
/// <summary>
/// A <see cref="SettingsSlider{TValue,TSlider}"/> that displays its value as a percentage by default.
/// Mostly provided for convenience of use with <see cref="SettingSourceAttribute"/>.
/// </summary>
public partial class SettingsPercentageSlider<TValue> : SettingsSlider<TValue>
where TValue : struct, IEquatable<TValue>, IComparable<TValue>, IConvertible
{
protected override Drawable CreateControl() => ((RoundedSliderBar<TValue>)base.CreateControl()).With(sliderBar => sliderBar.DisplayAsPercentage = true);
}
}

View File

@ -0,0 +1,75 @@
// 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.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK;
namespace osu.Game.Overlays.SkinEditor
{
public partial class NonSkinnableScreenPlaceholder : CompositeDrawable
{
[Resolved]
private SkinEditorOverlay? skinEditorOverlay { get; set; }
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colourProvider.Dark6,
RelativeSizeAxes = Axes.Both,
Alpha = 0.95f,
},
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0, 5),
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new SpriteIcon
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Icon = FontAwesome.Solid.ExclamationCircle,
Size = new Vector2(24),
Y = -5,
},
new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold, size: 18))
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
TextAnchor = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Text = "Please navigate to a skinnable screen using the scene library",
},
new RoundedButton
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 200,
Margin = new MarginPadding { Top = 20 },
Action = () => skinEditorOverlay?.Hide(),
Text = "Return to game"
}
}
},
};
}
}
}

View File

@ -7,15 +7,7 @@ using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Skinning;
@ -26,16 +18,16 @@ namespace osu.Game.Overlays.SkinEditor
{
public partial class SkinBlueprintContainer : BlueprintContainer<ISerialisableDrawable>
{
private readonly Drawable target;
private readonly ISerialisableDrawableContainer targetContainer;
private readonly List<BindableList<ISerialisableDrawable>> targetComponents = new List<BindableList<ISerialisableDrawable>>();
[Resolved]
private SkinEditor editor { get; set; } = null!;
public SkinBlueprintContainer(Drawable target)
public SkinBlueprintContainer(ISerialisableDrawableContainer targetContainer)
{
this.target = target;
this.targetContainer = targetContainer;
}
protected override void LoadComplete()
@ -44,22 +36,10 @@ namespace osu.Game.Overlays.SkinEditor
SelectedItems.BindTo(editor.SelectedComponents);
// track each target container on the current screen.
var targetContainers = target.ChildrenOfType<ISerialisableDrawableContainer>().ToArray();
var bindableList = new BindableList<ISerialisableDrawable> { BindTarget = targetContainer.Components };
bindableList.BindCollectionChanged(componentsChanged, true);
if (targetContainers.Length == 0)
{
AddInternal(new NonSkinnableScreenPlaceholder());
return;
}
foreach (var targetContainer in targetContainers)
{
var bindableList = new BindableList<ISerialisableDrawable> { BindTarget = targetContainer.Components };
bindableList.BindCollectionChanged(componentsChanged, true);
targetComponents.Add(bindableList);
}
targetComponents.Add(bindableList);
}
private void componentsChanged(object? sender, NotifyCollectionChangedEventArgs e) => Schedule(() =>
@ -160,65 +140,5 @@ namespace osu.Game.Overlays.SkinEditor
foreach (var list in targetComponents)
list.UnbindAll();
}
public partial class NonSkinnableScreenPlaceholder : CompositeDrawable
{
[Resolved]
private SkinEditorOverlay? skinEditorOverlay { get; set; }
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colourProvider.Dark6,
RelativeSizeAxes = Axes.Both,
Alpha = 0.95f,
},
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0, 5),
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new SpriteIcon
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Icon = FontAwesome.Solid.ExclamationCircle,
Size = new Vector2(24),
Y = -5,
},
new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold, size: 18))
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
TextAnchor = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Text = "Please navigate to a skinnable screen using the scene library",
},
new RoundedButton
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 200,
Margin = new MarginPadding { Top = 20 },
Action = () => skinEditorOverlay?.Hide(),
Text = "Return to game"
}
}
},
};
}
}
}
}

View File

@ -17,6 +17,7 @@ using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using Web = osu.Game.Resources.Localisation.Web;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Graphics;
@ -24,6 +25,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.OSD;
using osu.Game.Overlays.Settings;
using osu.Game.Screens.Edit;
@ -97,6 +99,9 @@ namespace osu.Game.Overlays.SkinEditor
[Resolved]
private OnScreenDisplay? onScreenDisplay { get; set; }
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
public SkinEditor()
{
}
@ -147,8 +152,8 @@ namespace osu.Game.Overlays.SkinEditor
{
Items = new[]
{
new EditorMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()),
new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, revert),
new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()),
new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))),
new EditorMenuItemSpacer(),
new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()),
},
@ -304,7 +309,8 @@ namespace osu.Game.Overlays.SkinEditor
changeHandler?.Dispose();
// Immediately clear the previous blueprint container to ensure it doesn't try to interact with the old target.
content?.Clear();
if (content?.Child is SkinBlueprintContainer)
content.Clear();
Scheduler.AddOnce(loadBlueprintContainer);
Scheduler.AddOnce(populateSettings);
@ -323,17 +329,18 @@ namespace osu.Game.Overlays.SkinEditor
foreach (var toolbox in componentsSidebar.OfType<SkinComponentToolbox>())
toolbox.Expire();
if (target.NewValue == null)
return;
componentsSidebar.Clear();
SelectedComponents.Clear();
Debug.Assert(content != null);
SelectedComponents.Clear();
var skinComponentsContainer = getTarget(target.NewValue);
if (skinComponentsContainer == null)
if (target.NewValue == null || skinComponentsContainer == null)
{
content.Child = new NonSkinnableScreenPlaceholder();
return;
}
changeHandler = new SkinEditorChangeHandler(skinComponentsContainer);
changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
@ -668,6 +675,16 @@ namespace osu.Game.Overlays.SkinEditor
}
}
public partial class RevertConfirmDialog : DangerousActionDialog
{
public RevertConfirmDialog(Action revert)
{
HeaderText = CommonStrings.RevertToDefault;
BodyText = SkinEditorStrings.RevertToDefaultDescription;
DangerousAction = revert;
}
}
#region Delegation of IEditorChangeHandler
public event Action? OnStateChange

View File

@ -7,7 +7,6 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Judgements;
@ -33,7 +32,7 @@ namespace osu.Game.Rulesets.Mods
public override string SettingDescription => base.SettingDescription.Replace(MinimumAccuracy.ToString(), MinimumAccuracy.Value.ToString("##%", NumberFormatInfo.InvariantInfo));
[SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(SettingsSlider<double, PercentSlider>))]
[SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(SettingsPercentageSlider<double>))]
public BindableNumber<double> MinimumAccuracy { get; } = new BindableDouble
{
MinValue = 0.60,
@ -69,12 +68,4 @@ namespace osu.Game.Rulesets.Mods
return scoreProcessor.ComputeAccuracy(score);
}
}
public partial class PercentSlider : RoundedSliderBar<double>
{
public PercentSlider()
{
DisplayAsPercentage = true;
}
}
}

View File

@ -7,12 +7,12 @@ using osu.Game.Overlays.Dialog;
namespace osu.Game.Screens.Edit
{
public partial class DeleteDifficultyConfirmationDialog : DeleteConfirmationDialog
public partial class DeleteDifficultyConfirmationDialog : DangerousActionDialog
{
public DeleteDifficultyConfirmationDialog(BeatmapInfo beatmapInfo, Action deleteAction)
{
BodyText = $"\"{beatmapInfo.DifficultyName}\" difficulty";
DeleteAction = deleteAction;
DangerousAction = deleteAction;
}
}
}

View File

@ -0,0 +1,58 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Localisation.SkinComponents;
using osu.Game.Overlays.Settings;
using osu.Game.Skinning;
using osu.Game.Users.Drawables;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
public partial class PlayerAvatar : CompositeDrawable, ISerialisableDrawable
{
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.CornerRadius), nameof(SkinnableComponentStrings.CornerRadiusDescription),
SettingControlType = typeof(SettingsPercentageSlider<float>))]
public new BindableFloat CornerRadius { get; set; } = new BindableFloat(0.25f)
{
MinValue = 0,
MaxValue = 0.5f,
Precision = 0.01f
};
private readonly UpdateableAvatar avatar;
private const float default_size = 80f;
public PlayerAvatar()
{
Size = new Vector2(default_size);
InternalChild = avatar = new UpdateableAvatar(isInteractive: false)
{
RelativeSizeAxes = Axes.Both,
Masking = true
};
}
[BackgroundDependencyLoader]
private void load(GameplayState gameplayState)
{
avatar.User = gameplayState.Score.ScoreInfo.User;
}
protected override void LoadComplete()
{
base.LoadComplete();
CornerRadius.BindValueChanged(e => avatar.CornerRadius = e.NewValue * default_size, true);
}
public bool UsesFixedAnchor { get; set; }
}
}

View File

@ -0,0 +1,36 @@
// 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.Graphics.Containers;
using osu.Game.Skinning;
using osu.Game.Users.Drawables;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
public partial class PlayerFlag : CompositeDrawable, ISerialisableDrawable
{
private readonly UpdateableFlag flag;
private const float default_size = 40f;
public PlayerFlag()
{
Size = new Vector2(default_size, default_size / 1.4f);
InternalChild = flag = new UpdateableFlag
{
RelativeSizeAxes = Axes.Both,
};
}
[BackgroundDependencyLoader]
private void load(GameplayState gameplayState)
{
flag.CountryCode = gameplayState.Score.ScoreInfo.User.CountryCode;
}
public bool UsesFixedAnchor { get; set; }
}
}

View File

@ -10,7 +10,7 @@ using osu.Game.Scoring;
namespace osu.Game.Screens.Select
{
public partial class BeatmapClearScoresDialog : DeleteConfirmationDialog
public partial class BeatmapClearScoresDialog : DangerousActionDialog
{
[Resolved]
private ScoreManager scoreManager { get; set; } = null!;
@ -18,7 +18,7 @@ namespace osu.Game.Screens.Select
public BeatmapClearScoresDialog(BeatmapInfo beatmapInfo, Action onCompletion)
{
BodyText = $"All local scores on {beatmapInfo.GetDisplayTitle()}";
DeleteAction = () =>
DangerousAction = () =>
{
Task.Run(() => scoreManager.Delete(beatmapInfo))
.ContinueWith(_ => onCompletion);

View File

@ -7,7 +7,7 @@ using osu.Game.Overlays.Dialog;
namespace osu.Game.Screens.Select
{
public partial class BeatmapDeleteDialog : DeleteConfirmationDialog
public partial class BeatmapDeleteDialog : DangerousActionDialog
{
private readonly BeatmapSetInfo beatmapSet;
@ -20,7 +20,7 @@ namespace osu.Game.Screens.Select
[BackgroundDependencyLoader]
private void load(BeatmapManager beatmapManager)
{
DeleteAction = () => beatmapManager.Delete(beatmapSet);
DangerousAction = () => beatmapManager.Delete(beatmapSet);
}
}
}

View File

@ -61,7 +61,7 @@ namespace osu.Game.Screens.Select.Carousel
if (!(other is CarouselBeatmapSet otherSet))
return base.CompareTo(criteria, other);
int comparison = 0;
int comparison;
switch (criteria.Sort)
{
@ -87,11 +87,7 @@ namespace osu.Game.Screens.Select.Carousel
break;
case SortMode.DateRanked:
// Beatmaps which have no ranked date should already be filtered away in this mode.
if (BeatmapSet.DateRanked == null || otherSet.BeatmapSet.DateRanked == null)
break;
comparison = otherSet.BeatmapSet.DateRanked.Value.CompareTo(BeatmapSet.DateRanked.Value);
comparison = Nullable.Compare(otherSet.BeatmapSet.DateRanked, BeatmapSet.DateRanked);
break;
case SortMode.LastPlayed:
@ -111,11 +107,7 @@ namespace osu.Game.Screens.Select.Carousel
break;
case SortMode.DateSubmitted:
// Beatmaps which have no submitted date should already be filtered away in this mode.
if (BeatmapSet.DateSubmitted == null || otherSet.BeatmapSet.DateSubmitted == null)
break;
comparison = otherSet.BeatmapSet.DateSubmitted.Value.CompareTo(BeatmapSet.DateSubmitted.Value);
comparison = Nullable.Compare(otherSet.BeatmapSet.DateSubmitted, BeatmapSet.DateSubmitted);
break;
}
@ -153,12 +145,7 @@ namespace osu.Game.Screens.Select.Carousel
{
base.Filter(criteria);
bool filtered = Items.All(i => i.Filtered.Value);
filtered |= criteria.Sort == SortMode.DateRanked && BeatmapSet.DateRanked == null;
filtered |= criteria.Sort == SortMode.DateSubmitted && BeatmapSet.DateSubmitted == null;
Filtered.Value = filtered;
Filtered.Value = Items.All(i => i.Filtered.Value);
}
public override string ToString() => BeatmapSet.ToString();

View File

@ -8,14 +8,14 @@ using osu.Game.Localisation;
namespace osu.Game.Screens.Select.Carousel
{
public partial class UpdateLocalConfirmationDialog : DeleteConfirmationDialog
public partial class UpdateLocalConfirmationDialog : DangerousActionDialog
{
public UpdateLocalConfirmationDialog(Action onConfirm)
{
HeaderText = PopupDialogStrings.UpdateLocallyModifiedText;
BodyText = PopupDialogStrings.UpdateLocallyModifiedDescription;
Icon = FontAwesome.Solid.ExclamationTriangle;
DeleteAction = onConfirm;
DangerousAction = onConfirm;
}
}
}

View File

@ -10,7 +10,7 @@ using osu.Game.Beatmaps;
namespace osu.Game.Screens.Select
{
public partial class LocalScoreDeleteDialog : DeleteConfirmationDialog
public partial class LocalScoreDeleteDialog : DangerousActionDialog
{
private readonly ScoreInfo score;
@ -28,7 +28,7 @@ namespace osu.Game.Screens.Select
BodyText = $"{score.User} ({score.DisplayAccuracy}, {score.Rank})";
Icon = FontAwesome.Regular.TrashAlt;
DeleteAction = () => scoreManager.Delete(score);
DangerousAction = () => scoreManager.Delete(score);
}
}
}

View File

@ -7,7 +7,7 @@ using osu.Game.Overlays.Dialog;
namespace osu.Game.Screens.Select
{
public partial class SkinDeleteDialog : DeleteConfirmationDialog
public partial class SkinDeleteDialog : DangerousActionDialog
{
private readonly Skin skin;
@ -20,7 +20,7 @@ namespace osu.Game.Screens.Select
[BackgroundDependencyLoader]
private void load(SkinManager manager)
{
DeleteAction = () =>
DangerousAction = () =>
{
manager.Delete(skin.SkinInfo.Value);
manager.CurrentSkinInfo.SetDefault();

View File

@ -58,7 +58,11 @@ namespace osu.Game.Storyboards.Drawables
using (drawableVideo.BeginAbsoluteSequence(Video.StartTime))
{
Schedule(() => drawableVideo.PlaybackPosition = Time.Current - Video.StartTime);
drawableVideo.FadeIn(500);
using (drawableVideo.BeginDelayedSequence(drawableVideo.Duration - 500))
drawableVideo.FadeOut(500);
}
}
}

View File

@ -1,8 +1,6 @@
// 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 disable
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@ -30,14 +28,14 @@ namespace osu.Game.Users.Drawables
/// Perform an action in addition to showing the country ranking.
/// This should be used to perform auxiliary tasks and not as a primary action for clicking a flag (to maintain a consistent UX).
/// </summary>
public Action Action;
public Action? Action;
public UpdateableFlag(CountryCode countryCode = CountryCode.Unknown)
{
CountryCode = countryCode;
}
protected override Drawable CreateDrawable(CountryCode countryCode)
protected override Drawable? CreateDrawable(CountryCode countryCode)
{
if (countryCode == CountryCode.Unknown && !ShowPlaceholderOnUnknown)
return null;
@ -56,8 +54,8 @@ namespace osu.Game.Users.Drawables
};
}
[Resolved(canBeNull: true)]
private RankingsOverlay rankingsOverlay { get; set; }
[Resolved]
private RankingsOverlay? rankingsOverlay { get; set; }
protected override bool OnClick(ClickEvent e)
{