diff --git a/UseLocalFramework.ps1 b/UseLocalFramework.ps1 index 837685f310..9f4547d980 100644 --- a/UseLocalFramework.ps1 +++ b/UseLocalFramework.ps1 @@ -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 "") + { + " " + } + + $_ + } | 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 diff --git a/UseLocalFramework.sh b/UseLocalFramework.sh index 4fd1fdfd1b..c12b388e96 100755 --- a/UseLocalFramework.sh +++ b/UseLocalFramework.sh @@ -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\ + \ +' ./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 diff --git a/osu.Game.Benchmarks/BenchmarkCarouselFilter.cs b/osu.Game.Benchmarks/BenchmarkCarouselFilter.cs new file mode 100644 index 0000000000..8f7027da17 --- /dev/null +++ b/osu.Game.Benchmarks/BenchmarkCarouselFilter.cs @@ -0,0 +1,123 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using BenchmarkDotNet.Attributes; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Carousel; + +namespace osu.Game.Benchmarks +{ + public class BenchmarkCarouselFilter : BenchmarkTest + { + private BeatmapInfo getExampleBeatmap() => new BeatmapInfo + { + Ruleset = new RulesetInfo + { + ShortName = "osu", + OnlineID = 0 + }, + StarRating = 4.0d, + Difficulty = new BeatmapDifficulty + { + ApproachRate = 5.0f, + DrainRate = 3.0f, + CircleSize = 2.0f, + }, + Metadata = new BeatmapMetadata + { + Artist = "The Artist", + ArtistUnicode = "check unicode too", + Title = "Title goes here", + TitleUnicode = "Title goes here", + Author = { Username = "The Author" }, + Source = "unit tests", + Tags = "look for tags too", + }, + DifficultyName = "version as well", + Length = 2500, + BPM = 160, + BeatDivisor = 12, + Status = BeatmapOnlineStatus.Loved + }; + + private CarouselBeatmap carouselBeatmap = null!; + private FilterCriteria criteria1 = null!; + private FilterCriteria criteria2 = null!; + private FilterCriteria criteria3 = null!; + private FilterCriteria criteria4 = null!; + private FilterCriteria criteria5 = null!; + private FilterCriteria criteria6 = null!; + + public override void SetUp() + { + var beatmap = getExampleBeatmap(); + beatmap.OnlineID = 20201010; + beatmap.BeatmapSet = new BeatmapSetInfo { OnlineID = 1535 }; + carouselBeatmap = new CarouselBeatmap(beatmap); + criteria1 = new FilterCriteria(); + criteria2 = new FilterCriteria + { + Ruleset = new RulesetInfo { ShortName = "catch" } + }; + criteria3 = new FilterCriteria + { + Ruleset = new RulesetInfo { OnlineID = 6 }, + AllowConvertedBeatmaps = true, + BPM = new FilterCriteria.OptionalRange + { + IsUpperInclusive = false, + Max = 160d + } + }; + criteria4 = new FilterCriteria + { + Ruleset = new RulesetInfo { OnlineID = 6 }, + AllowConvertedBeatmaps = true, + SearchText = "an artist" + }; + criteria5 = new FilterCriteria + { + Creator = new FilterCriteria.OptionalTextFilter { SearchTerm = "the author AND then something else" } + }; + criteria6 = new FilterCriteria { SearchText = "20201010" }; + } + + [Benchmark] + public void CarouselBeatmapFilter() + { + carouselBeatmap.Filter(criteria1); + } + + [Benchmark] + public void CriteriaMatchingSpecificRuleset() + { + carouselBeatmap.Filter(criteria2); + } + + [Benchmark] + public void CriteriaMatchingRangeMax() + { + carouselBeatmap.Filter(criteria3); + } + + [Benchmark] + public void CriteriaMatchingTerms() + { + carouselBeatmap.Filter(criteria4); + } + + [Benchmark] + public void CriteriaMatchingCreator() + { + carouselBeatmap.Filter(criteria5); + } + + [Benchmark] + public void CriteriaMatchingBeatmapIDs() + { + carouselBeatmap.Filter(criteria6); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSingleTap.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSingleTap.cs new file mode 100644 index 0000000000..0cd3b85f8e --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSingleTap.cs @@ -0,0 +1,212 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Taiko.Mods; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Replays; + +namespace osu.Game.Rulesets.Taiko.Tests.Mods +{ + public partial class TestSceneTaikoModSingleTap : TaikoModTestScene + { + [Test] + public void TestInputAlternate() => CreateModTest(new ModTestData + { + Mod = new TaikoModSingleTap(), + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Hit + { + StartTime = 100, + Type = HitType.Rim + }, + new Hit + { + StartTime = 300, + Type = HitType.Rim + }, + new Hit + { + StartTime = 500, + Type = HitType.Rim + }, + new Hit + { + StartTime = 700, + Type = HitType.Rim + }, + }, + }, + ReplayFrames = new List + { + new TaikoReplayFrame(100, TaikoAction.RightRim), + new TaikoReplayFrame(120), + new TaikoReplayFrame(300, TaikoAction.LeftRim), + new TaikoReplayFrame(320), + new TaikoReplayFrame(500, TaikoAction.RightRim), + new TaikoReplayFrame(520), + new TaikoReplayFrame(700, TaikoAction.LeftRim), + new TaikoReplayFrame(720), + }, + PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 1 + }); + + [Test] + public void TestInputSameKey() => CreateModTest(new ModTestData + { + Mod = new TaikoModSingleTap(), + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Hit + { + StartTime = 100, + Type = HitType.Rim + }, + new Hit + { + StartTime = 300, + Type = HitType.Rim + }, + new Hit + { + StartTime = 500, + Type = HitType.Rim + }, + new Hit + { + StartTime = 700, + Type = HitType.Rim + }, + }, + }, + ReplayFrames = new List + { + new TaikoReplayFrame(100, TaikoAction.RightRim), + new TaikoReplayFrame(120), + new TaikoReplayFrame(300, TaikoAction.RightRim), + new TaikoReplayFrame(320), + new TaikoReplayFrame(500, TaikoAction.RightRim), + new TaikoReplayFrame(520), + new TaikoReplayFrame(700, TaikoAction.RightRim), + new TaikoReplayFrame(720), + }, + PassCondition = () => Player.ScoreProcessor.Combo.Value == 4 + }); + + [Test] + public void TestInputIntro() => CreateModTest(new ModTestData + { + Mod = new TaikoModSingleTap(), + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Hit + { + StartTime = 100, + Type = HitType.Rim + }, + }, + }, + ReplayFrames = new List + { + new TaikoReplayFrame(0, TaikoAction.RightRim), + new TaikoReplayFrame(20), + new TaikoReplayFrame(100, TaikoAction.LeftRim), + new TaikoReplayFrame(120), + }, + PassCondition = () => Player.ScoreProcessor.Combo.Value == 1 + }); + + [Test] + public void TestInputStrong() => CreateModTest(new ModTestData + { + Mod = new TaikoModSingleTap(), + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Hit + { + StartTime = 100, + Type = HitType.Rim + }, + new Hit + { + StartTime = 300, + Type = HitType.Rim, + IsStrong = true + }, + new Hit + { + StartTime = 500, + Type = HitType.Rim, + }, + }, + }, + ReplayFrames = new List + { + new TaikoReplayFrame(100, TaikoAction.RightRim), + new TaikoReplayFrame(120), + new TaikoReplayFrame(300, TaikoAction.LeftRim), + new TaikoReplayFrame(320), + new TaikoReplayFrame(500, TaikoAction.LeftRim), + new TaikoReplayFrame(520), + }, + PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2 + }); + + [Test] + public void TestInputBreaks() => CreateModTest(new ModTestData + { + Mod = new TaikoModSingleTap(), + Autoplay = false, + Beatmap = new Beatmap + { + Breaks = new List + { + new BreakPeriod(100, 1600), + }, + HitObjects = new List + { + new Hit + { + StartTime = 100, + Type = HitType.Rim + }, + new Hit + { + StartTime = 2000, + Type = HitType.Rim, + }, + }, + }, + ReplayFrames = new List + { + new TaikoReplayFrame(100, TaikoAction.RightRim), + new TaikoReplayFrame(120), + // Press different key after break but before hit object. + new TaikoReplayFrame(1900, TaikoAction.LeftRim), + new TaikoReplayFrame(1820), + // Press original key at second hitobject and ensure it has been hit. + new TaikoReplayFrame(2000, TaikoAction.RightRim), + new TaikoReplayFrame(2020), + }, + PassCondition = () => Player.ScoreProcessor.Combo.Value == 2 + }); + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineGeneration.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineGeneration.cs index 7b8c8926f0..8511ad9f00 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineGeneration.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineGeneration.cs @@ -73,11 +73,10 @@ namespace osu.Game.Rulesets.Taiko.Tests beatmap.ControlPointInfo.Add(start_time, new TimingControlPoint { BeatLength = beat_length, - TimeSignature = new TimeSignature(time_signature_numerator) + TimeSignature = new TimeSignature(time_signature_numerator), + OmitFirstBarLine = true }); - beatmap.ControlPointInfo.Add(start_time, new EffectControlPoint { OmitFirstBarLine = true }); - var barlines = new BarLineGenerator(beatmap).BarLines; AddAssert("first barline ommited", () => barlines.All(b => b.StartTime != start_time)); diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 3cc47deed0..6a35e9376b 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -72,7 +72,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps converted.ControlPointInfo.Add(hitObject.StartTime, new EffectControlPoint { KiaiMode = currentEffectPoint.KiaiMode, - OmitFirstBarLine = currentEffectPoint.OmitFirstBarLine, ScrollSpeed = lastScrollSpeed = nextScrollSpeed, }); } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs index 4b74b4991e..1e09061859 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Replays; @@ -12,5 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) => new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" }); + + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModSingleTap) }).ToArray(); } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs index fee0cb2744..c268087f0a 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Objects; @@ -13,5 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) => new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" }); + + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModSingleTap) }).ToArray(); } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs index 69a98f8372..e90ab589fc 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Linq; using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; @@ -9,5 +11,7 @@ namespace osu.Game.Rulesets.Taiko.Mods public class TaikoModRelax : ModRelax { public override LocalisableString Description => @"No ninja-like spinners, demanding drumrolls or unexpected katus."; + + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModSingleTap) }).ToArray(); } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs new file mode 100644 index 0000000000..a5cffca06f --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs @@ -0,0 +1,127 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; +using osu.Game.Utils; +using osu.Game.Rulesets.Taiko.UI; + +namespace osu.Game.Rulesets.Taiko.Mods +{ + public partial class TaikoModSingleTap : Mod, IApplicableToDrawableRuleset, IUpdatableByPlayfield + { + public override string Name => @"Single Tap"; + public override string Acronym => @"SG"; + public override LocalisableString Description => @"One key for dons, one key for kats."; + + public override double ScoreMultiplier => 1.0; + public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(TaikoModCinema) }; + public override ModType Type => ModType.Conversion; + + private DrawableTaikoRuleset ruleset = null!; + + private TaikoPlayfield playfield { get; set; } = null!; + + private TaikoAction? lastAcceptedCentreAction { get; set; } + private TaikoAction? lastAcceptedRimAction { get; set; } + + /// + /// A tracker for periods where single tap should not be enforced (i.e. non-gameplay periods). + /// + /// + /// This is different from in that the periods here end strictly at the first object after the break, rather than the break's end time. + /// + private PeriodTracker nonGameplayPeriods = null!; + + private IFrameStableClock gameplayClock = null!; + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + ruleset = (DrawableTaikoRuleset)drawableRuleset; + ruleset.KeyBindingInputManager.Add(new InputInterceptor(this)); + playfield = (TaikoPlayfield)ruleset.Playfield; + + var periods = new List(); + + if (drawableRuleset.Objects.Any()) + { + periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1)); + + foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks) + periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1)); + + static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh); + } + + nonGameplayPeriods = new PeriodTracker(periods); + + gameplayClock = drawableRuleset.FrameStableClock; + } + + public void Update(Playfield playfield) + { + if (!nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime)) return; + + lastAcceptedCentreAction = null; + lastAcceptedRimAction = null; + } + + private bool checkCorrectAction(TaikoAction action) + { + if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime)) + return true; + + // If next hit object is strong, allow usage of all actions. Strong drumrolls are ignored in this check. + if (playfield.HitObjectContainer.AliveObjects.FirstOrDefault(h => h.Result?.HasResult != true)?.HitObject is TaikoStrongableHitObject hitObject + && hitObject.IsStrong + && hitObject is not DrumRoll) + return true; + + if ((action == TaikoAction.LeftCentre || action == TaikoAction.RightCentre) + && (lastAcceptedCentreAction == null || lastAcceptedCentreAction == action)) + { + lastAcceptedCentreAction = action; + return true; + } + + if ((action == TaikoAction.LeftRim || action == TaikoAction.RightRim) + && (lastAcceptedRimAction == null || lastAcceptedRimAction == action)) + { + lastAcceptedRimAction = action; + return true; + } + + return false; + } + + private partial class InputInterceptor : Component, IKeyBindingHandler + { + private readonly TaikoModSingleTap mod; + + public InputInterceptor(TaikoModSingleTap mod) + { + this.mod = mod; + } + + public bool OnPressed(KeyBindingPressEvent e) + // if the pressed action is incorrect, block it from reaching gameplay. + => !mod.checkCorrectAction(e.Action); + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 4f435e73b3..a35fdb890d 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -158,6 +158,7 @@ namespace osu.Game.Rulesets.Taiko new TaikoModDifficultyAdjust(), new TaikoModClassic(), new TaikoModSwap(), + new TaikoModSingleTap(), }; case ModType.Automation: diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index 40203440c5..a08877e2dd 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -33,6 +33,8 @@ namespace osu.Game.Rulesets.Taiko.UI public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true); + public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager; + protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping; protected override bool UserScrollSpeedAdjustment => false; diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 5787bd6066..85d304da9c 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -181,16 +181,19 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(956, timingPoint.Time); Assert.AreEqual(329.67032967033, timingPoint.BeatLength); Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature); + Assert.IsFalse(timingPoint.OmitFirstBarLine); timingPoint = controlPoints.TimingPointAt(48428); Assert.AreEqual(956, timingPoint.Time); Assert.AreEqual(329.67032967033d, timingPoint.BeatLength); Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature); + Assert.IsFalse(timingPoint.OmitFirstBarLine); timingPoint = controlPoints.TimingPointAt(119637); Assert.AreEqual(119637, timingPoint.Time); Assert.AreEqual(659.340659340659, timingPoint.BeatLength); Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature); + Assert.IsFalse(timingPoint.OmitFirstBarLine); var difficultyPoint = controlPoints.DifficultyPointAt(0); Assert.AreEqual(0, difficultyPoint.Time); @@ -222,17 +225,14 @@ namespace osu.Game.Tests.Beatmaps.Formats var effectPoint = controlPoints.EffectPointAt(0); Assert.AreEqual(0, effectPoint.Time); Assert.IsFalse(effectPoint.KiaiMode); - Assert.IsFalse(effectPoint.OmitFirstBarLine); effectPoint = controlPoints.EffectPointAt(53703); Assert.AreEqual(53703, effectPoint.Time); Assert.IsTrue(effectPoint.KiaiMode); - Assert.IsFalse(effectPoint.OmitFirstBarLine); effectPoint = controlPoints.EffectPointAt(116637); Assert.AreEqual(95901, effectPoint.Time); Assert.IsFalse(effectPoint.KiaiMode); - Assert.IsFalse(effectPoint.OmitFirstBarLine); } } @@ -273,6 +273,28 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestDecodeOmitBarLineEffect() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("omit-barline-control-points.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var controlPoints = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo; + + Assert.That(controlPoints.TimingPoints.Count, Is.EqualTo(6)); + Assert.That(controlPoints.EffectPoints.Count, Is.EqualTo(0)); + + Assert.That(controlPoints.TimingPointAt(500).OmitFirstBarLine, Is.False); + Assert.That(controlPoints.TimingPointAt(1500).OmitFirstBarLine, Is.True); + Assert.That(controlPoints.TimingPointAt(2500).OmitFirstBarLine, Is.False); + Assert.That(controlPoints.TimingPointAt(3500).OmitFirstBarLine, Is.False); + Assert.That(controlPoints.TimingPointAt(4500).OmitFirstBarLine, Is.False); + Assert.That(controlPoints.TimingPointAt(5500).OmitFirstBarLine, Is.True); + } + } + [Test] public void TestTimingPointResetsSpeedMultiplier() { diff --git a/osu.Game.Tests/Database/LegacyExporterTest.cs b/osu.Game.Tests/Database/LegacyExporterTest.cs new file mode 100644 index 0000000000..d41b3a5017 --- /dev/null +++ b/osu.Game.Tests/Database/LegacyExporterTest.cs @@ -0,0 +1,125 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Database; + +namespace osu.Game.Tests.Database +{ + [TestFixture] + public class LegacyExporterTest + { + private TestLegacyExporter legacyExporter = null!; + private TemporaryNativeStorage storage = null!; + + private const string short_filename = "normal file name"; + + private const string long_filename = + "some file with super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name"; + + [SetUp] + public void SetUp() + { + storage = new TemporaryNativeStorage("export-storage"); + legacyExporter = new TestLegacyExporter(storage); + } + + [Test] + public void ExportFileWithNormalNameTest() + { + var item = new TestPathInfo(short_filename); + + Assert.That(item.Filename.Length, Is.LessThan(TestLegacyExporter.MAX_FILENAME_LENGTH)); + + exportItemAndAssert(item, short_filename); + } + + [Test] + public void ExportFileWithNormalNameMultipleTimesTest() + { + var item = new TestPathInfo(short_filename); + + Assert.That(item.Filename.Length, Is.LessThan(TestLegacyExporter.MAX_FILENAME_LENGTH)); + + //Export multiple times + for (int i = 0; i < 100; i++) + { + string expectedFileName = i == 0 ? short_filename : $"{short_filename} ({i})"; + exportItemAndAssert(item, expectedFileName); + } + } + + [Test] + public void ExportFileWithSuperLongNameTest() + { + int expectedLength = TestLegacyExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length); + string expectedName = long_filename.Remove(expectedLength); + + var item = new TestPathInfo(long_filename); + + Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyExporter.MAX_FILENAME_LENGTH)); + exportItemAndAssert(item, expectedName); + } + + [Test] + public void ExportFileWithSuperLongNameMultipleTimesTest() + { + int expectedLength = TestLegacyExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length); + string expectedName = long_filename.Remove(expectedLength); + + var item = new TestPathInfo(long_filename); + + Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyExporter.MAX_FILENAME_LENGTH)); + + //Export multiple times + for (int i = 0; i < 100; i++) + { + string expectedFilename = i == 0 ? expectedName : $"{expectedName} ({i})"; + exportItemAndAssert(item, expectedFilename); + } + } + + private void exportItemAndAssert(IHasNamedFiles item, string expectedName) + { + Assert.DoesNotThrow(() => legacyExporter.Export(item)); + Assert.That(storage.Exists($"exports/{expectedName}{legacyExporter.GetExtension()}"), Is.True); + } + + [TearDown] + public void TearDown() + { + if (storage.IsNotNull()) + storage.Dispose(); + } + + private class TestPathInfo : IHasNamedFiles + { + public string Filename { get; } + + public IEnumerable Files { get; } = new List(); + + public TestPathInfo(string filename) + { + Filename = filename; + } + + public override string ToString() => Filename; + } + + private class TestLegacyExporter : LegacyExporter + { + public TestLegacyExporter(Storage storage) + : base(storage) + { + } + + public string GetExtension() => FileExtension; + + protected override string FileExtension => ".test"; + } + } +} diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs index 554473cf77..a2ded643fa 100644 --- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs +++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs @@ -43,6 +43,18 @@ namespace osu.Game.Tests.NonVisual Assert.That(cpi.Groups.Count, Is.EqualTo(2)); Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2)); Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2)); + + cpi.Add(1200, new TimingControlPoint { OmitFirstBarLine = true }); // is not redundant + + Assert.That(cpi.Groups.Count, Is.EqualTo(3)); + Assert.That(cpi.TimingPoints.Count, Is.EqualTo(3)); + Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(3)); + + cpi.Add(1500, new TimingControlPoint { OmitFirstBarLine = true }); // is not redundant + + Assert.That(cpi.Groups.Count, Is.EqualTo(4)); + Assert.That(cpi.TimingPoints.Count, Is.EqualTo(4)); + Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(4)); } [Test] @@ -95,12 +107,12 @@ namespace osu.Game.Tests.NonVisual Assert.That(cpi.EffectPoints.Count, Is.EqualTo(0)); Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0)); - cpi.Add(1000, new EffectControlPoint { KiaiMode = true, OmitFirstBarLine = true }); // is not redundant - cpi.Add(1400, new EffectControlPoint { KiaiMode = true, OmitFirstBarLine = true }); // same settings, but is not redundant + cpi.Add(1000, new EffectControlPoint { KiaiMode = true }); // is not redundant + cpi.Add(1400, new EffectControlPoint { KiaiMode = true }); // is redundant - Assert.That(cpi.Groups.Count, Is.EqualTo(2)); - Assert.That(cpi.EffectPoints.Count, Is.EqualTo(2)); - Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2)); + Assert.That(cpi.Groups.Count, Is.EqualTo(1)); + Assert.That(cpi.EffectPoints.Count, Is.EqualTo(1)); + Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1)); } [Test] diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20230305.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20230305.osk new file mode 100644 index 0000000000..c85f3b6352 Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/modified-argon-20230305.osk differ diff --git a/osu.Game.Tests/Resources/omit-barline-control-points.osu b/osu.Game.Tests/Resources/omit-barline-control-points.osu new file mode 100644 index 0000000000..839a59215b --- /dev/null +++ b/osu.Game.Tests/Resources/omit-barline-control-points.osu @@ -0,0 +1,27 @@ +osu file format v14 + +[TimingPoints] + +// Uninherited: none, inherited: none +0,500,4,2,0,100,1,0 +0,-50,4,3,0,100,0,0 + +// Uninherited: omit, inherited: none +1000,500,4,2,0,100,1,8 +1000,-50,4,3,0,100,0,0 + +// Uninherited: none, inherited: omit (should be ignored, inheriting cannot omit) +2000,500,4,2,0,100,1,0 +2000,-50,4,3,0,100,0,8 + +// Inherited: none, uninherited: none +3000,-50,4,3,0,100,0,0 +3000,500,4,2,0,100,1,0 + +// Inherited: omit, uninherited: none (should be ignored, inheriting cannot omit) +4000,-50,4,3,0,100,0,8 +4000,500,4,2,0,100,1,0 + +// Inherited: none, uninherited: omit +5000,-50,4,3,0,100,0,0 +5000,500,4,2,0,100,1,8 diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 04b8c6dd4e..bd8088cfb6 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -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", }; /// diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index eab66b9857..3aab9a24e1 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; +using System.Collections.Generic; using osu.Framework.Localisation; namespace osu.Game.Beatmaps @@ -29,10 +29,21 @@ namespace osu.Game.Beatmaps return new RomanisableString($"{metadata.GetPreferred(true)}".Trim(), $"{metadata.GetPreferred(false)}".Trim()); } - public static string[] GetSearchableTerms(this IBeatmapInfo beatmapInfo) => new[] + public static List GetSearchableTerms(this IBeatmapInfo beatmapInfo) { - beatmapInfo.DifficultyName - }.Concat(beatmapInfo.Metadata.GetSearchableTerms()).Where(s => !string.IsNullOrEmpty(s)).ToArray(); + var termsList = new List(BeatmapMetadataInfoExtensions.MAX_SEARCHABLE_TERM_COUNT + 1); + + addIfNotNull(beatmapInfo.DifficultyName); + + BeatmapMetadataInfoExtensions.CollectSearchableTerms(beatmapInfo.Metadata, termsList); + return termsList; + + void addIfNotNull(string? s) + { + if (!string.IsNullOrEmpty(s)) + termsList.Add(s); + } + } private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]"; } diff --git a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs index 738bdb0839..fe4e815e62 100644 --- a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs @@ -3,7 +3,7 @@ #nullable disable -using System.Linq; +using System.Collections.Generic; using osu.Framework.Localisation; namespace osu.Game.Beatmaps @@ -13,16 +13,31 @@ namespace osu.Game.Beatmaps /// /// An array of all searchable terms provided in contained metadata. /// - public static string[] GetSearchableTerms(this IBeatmapMetadataInfo metadataInfo) => new[] + public static string[] GetSearchableTerms(this IBeatmapMetadataInfo metadataInfo) { - metadataInfo.Author.Username, - metadataInfo.Artist, - metadataInfo.ArtistUnicode, - metadataInfo.Title, - metadataInfo.TitleUnicode, - metadataInfo.Source, - metadataInfo.Tags - }.Where(s => !string.IsNullOrEmpty(s)).ToArray(); + var termsList = new List(MAX_SEARCHABLE_TERM_COUNT); + CollectSearchableTerms(metadataInfo, termsList); + return termsList.ToArray(); + } + + internal const int MAX_SEARCHABLE_TERM_COUNT = 7; + + internal static void CollectSearchableTerms(IBeatmapMetadataInfo metadataInfo, IList termsList) + { + addIfNotNull(metadataInfo.Author.Username); + addIfNotNull(metadataInfo.Artist); + addIfNotNull(metadataInfo.ArtistUnicode); + addIfNotNull(metadataInfo.Title); + addIfNotNull(metadataInfo.TitleUnicode); + addIfNotNull(metadataInfo.Source); + addIfNotNull(metadataInfo.Tags); + + void addIfNotNull(string s) + { + if (!string.IsNullOrEmpty(s)) + termsList.Add(s); + } + } /// /// A user-presentable display title representing this metadata. diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index 7c4313a015..7edf892f35 100644 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -13,15 +13,9 @@ namespace osu.Game.Beatmaps.ControlPoints public static readonly EffectControlPoint DEFAULT = new EffectControlPoint { KiaiModeBindable = { Disabled = true }, - OmitFirstBarLineBindable = { Disabled = true }, ScrollSpeedBindable = { Disabled = true } }; - /// - /// Whether the first bar line of this control point is ignored. - /// - public readonly BindableBool OmitFirstBarLineBindable = new BindableBool(); - /// /// The relative scroll speed at this control point. /// @@ -43,15 +37,6 @@ namespace osu.Game.Beatmaps.ControlPoints public override Color4 GetRepresentingColour(OsuColour colours) => colours.Purple; - /// - /// Whether the first bar line of this control point is ignored. - /// - public bool OmitFirstBarLine - { - get => OmitFirstBarLineBindable.Value; - set => OmitFirstBarLineBindable.Value = value; - } - /// /// Whether this control point enables Kiai mode. /// @@ -67,16 +52,13 @@ namespace osu.Game.Beatmaps.ControlPoints } public override bool IsRedundant(ControlPoint? existing) - => !OmitFirstBarLine - && existing is EffectControlPoint existingEffect + => existing is EffectControlPoint existingEffect && KiaiMode == existingEffect.KiaiMode - && OmitFirstBarLine == existingEffect.OmitFirstBarLine && ScrollSpeed == existingEffect.ScrollSpeed; public override void CopyFrom(ControlPoint other) { KiaiMode = ((EffectControlPoint)other).KiaiMode; - OmitFirstBarLine = ((EffectControlPoint)other).OmitFirstBarLine; ScrollSpeed = ((EffectControlPoint)other).ScrollSpeed; base.CopyFrom(other); @@ -88,10 +70,9 @@ namespace osu.Game.Beatmaps.ControlPoints public bool Equals(EffectControlPoint? other) => base.Equals(other) - && OmitFirstBarLine == other.OmitFirstBarLine && ScrollSpeed == other.ScrollSpeed && KiaiMode == other.KiaiMode; - public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), OmitFirstBarLine, ScrollSpeed, KiaiMode); + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), ScrollSpeed, KiaiMode); } } diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 61cc060594..4e69486e2d 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -16,6 +16,11 @@ namespace osu.Game.Beatmaps.ControlPoints /// public readonly Bindable TimeSignatureBindable = new Bindable(TimeSignature.SimpleQuadruple); + /// + /// Whether the first bar line of this control point is ignored. + /// + public readonly BindableBool OmitFirstBarLineBindable = new BindableBool(); + /// /// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing. /// @@ -30,6 +35,7 @@ namespace osu.Game.Beatmaps.ControlPoints Value = default_beat_length, Disabled = true }, + OmitFirstBarLineBindable = { Disabled = true }, TimeSignatureBindable = { Disabled = true } }; @@ -42,6 +48,15 @@ namespace osu.Game.Beatmaps.ControlPoints set => TimeSignatureBindable.Value = value; } + /// + /// Whether the first bar line of this control point is ignored. + /// + public bool OmitFirstBarLine + { + get => OmitFirstBarLineBindable.Value; + set => OmitFirstBarLineBindable.Value = value; + } + public const double DEFAULT_BEAT_LENGTH = 1000; /// @@ -73,6 +88,7 @@ namespace osu.Game.Beatmaps.ControlPoints public override void CopyFrom(ControlPoint other) { TimeSignature = ((TimingControlPoint)other).TimeSignature; + OmitFirstBarLine = ((TimingControlPoint)other).OmitFirstBarLine; BeatLength = ((TimingControlPoint)other).BeatLength; base.CopyFrom(other); @@ -85,8 +101,9 @@ namespace osu.Game.Beatmaps.ControlPoints public bool Equals(TimingControlPoint? other) => base.Equals(other) && TimeSignature.Equals(other.TimeSignature) + && OmitFirstBarLine == other.OmitFirstBarLine && BeatLength.Equals(other.BeatLength); - public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), TimeSignature, BeatLength); + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), TimeSignature, BeatLength, OmitFirstBarLine); } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 9c710b690e..eabc63b341 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -431,6 +431,7 @@ namespace osu.Game.Beatmaps.Formats controlPoint.BeatLength = beatLength; controlPoint.TimeSignature = timeSignature; + controlPoint.OmitFirstBarLine = omitFirstBarSignature; addControlPoint(time, controlPoint, true); } @@ -447,7 +448,6 @@ namespace osu.Game.Beatmaps.Formats var effectPoint = new EffectControlPoint { KiaiMode = kiaiMode, - OmitFirstBarLine = omitFirstBarSignature, }; // osu!taiko and osu!mania use effect points rather than difficulty points for scroll speed adjustments. diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 7e058d755e..072e442dea 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -222,6 +222,7 @@ namespace osu.Game.Beatmaps.Formats { var samplePoint = legacyControlPoints.SamplePointAt(time); var effectPoint = legacyControlPoints.EffectPointAt(time); + var timingPoint = legacyControlPoints.TimingPointAt(time); // Apply the control point to a hit sample to uncover legacy properties (e.g. suffix) HitSampleInfo tempHitSample = samplePoint.ApplyTo(new ConvertHitObjectParser.LegacyHitSampleInfo(string.Empty)); @@ -230,10 +231,10 @@ namespace osu.Game.Beatmaps.Formats LegacyEffectFlags effectFlags = LegacyEffectFlags.None; if (effectPoint.KiaiMode) effectFlags |= LegacyEffectFlags.Kiai; - if (effectPoint.OmitFirstBarLine) + if (timingPoint.OmitFirstBarLine) effectFlags |= LegacyEffectFlags.OmitFirstBarLine; - writer.Write(FormattableString.Invariant($"{legacyControlPoints.TimingPointAt(time).TimeSignature.Numerator},")); + writer.Write(FormattableString.Invariant($"{timingPoint.TimeSignature.Numerator},")); writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},")); writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},")); writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},")); diff --git a/osu.Game/Collections/DeleteCollectionDialog.cs b/osu.Game/Collections/DeleteCollectionDialog.cs index 4b23f661f9..9edc213077 100644 --- a/osu.Game/Collections/DeleteCollectionDialog.cs +++ b/osu.Game/Collections/DeleteCollectionDialog.cs @@ -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 collection, Action deleteAction) { BodyText = collection.PerformRead(c => $"{c.Name} ({"beatmap".ToQuantity(c.BeatmapMD5Hashes.Count)})"); - DeleteAction = deleteAction; + DangerousAction = deleteAction; } } } diff --git a/osu.Game/Database/LegacyExporter.cs b/osu.Game/Database/LegacyExporter.cs index 7fe58240a2..8da285daa3 100644 --- a/osu.Game/Database/LegacyExporter.cs +++ b/osu.Game/Database/LegacyExporter.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -19,6 +20,19 @@ namespace osu.Game.Database public abstract class LegacyExporter where TModel : class, IHasNamedFiles { + /// + /// Max length of filename (including extension). + /// + /// + /// + /// The filename limit for most OSs is 255. This actual usable length is smaller because adds an additional "_" to the end of the path. + /// + /// + /// For more information see file specification syntax, file systems limitations + /// + /// + public const int MAX_FILENAME_LENGTH = 255 - (32 + 4 + 2 + 5); //max path - (Guid + Guid "D" format chars + Storage.CreateFileSafely chars + account for ' (99)' suffix) + /// /// The file extension for exports (including the leading '.'). /// @@ -44,12 +58,16 @@ namespace osu.Game.Database { string itemFilename = GetFilename(item).GetValidFilename(); + if (itemFilename.Length > MAX_FILENAME_LENGTH - FileExtension.Length) + itemFilename = itemFilename.Remove(MAX_FILENAME_LENGTH - FileExtension.Length); + IEnumerable existingExports = exportStorage .GetFiles(string.Empty, $"{itemFilename}*{FileExtension}") .Concat(exportStorage.GetDirectories(string.Empty)); string filename = NamingUtils.GetNextBestFilename(existingExports, $"{itemFilename}{FileExtension}"); + using (var stream = exportStorage.CreateFileSafely(filename)) ExportModelTo(item, stream); diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 724005a0cc..42b30f9d18 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -114,7 +114,7 @@ namespace osu.Game.Graphics.Containers while (beatLength < MinimumBeatLength) beatLength *= 2; - int beatIndex = (int)((currentTrackTime - timingPoint.Time) / beatLength) - (effectPoint.OmitFirstBarLine ? 1 : 0); + int beatIndex = (int)((currentTrackTime - timingPoint.Time) / beatLength) - (timingPoint.OmitFirstBarLine ? 1 : 0); // The beats before the start of the first control point are off by 1, this should do the trick if (currentTrackTime < timingPoint.Time) diff --git a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs index 547df86fc7..7c11ea6ac6 100644 --- a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs +++ b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs @@ -39,6 +39,16 @@ namespace osu.Game.Localisation.SkinComponents /// public static LocalisableString TextElementTextDescription => new TranslatableString(getKey(@"text_element_text_description"), "The text to be displayed."); + /// + /// "Corner radius" + /// + public static LocalisableString CornerRadius => new TranslatableString(getKey(@"corner_radius"), "Corner radius"); + + /// + /// "How rounded the corners should be." + /// + public static LocalisableString CornerRadiusDescription => new TranslatableString(getKey(@"corner_radius_description"), "How rounded the corners should be."); + private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/SkinEditorStrings.cs b/osu.Game/Localisation/SkinEditorStrings.cs index 5cf2e5b5c5..3c1d1ff40d 100644 --- a/osu.Game/Localisation/SkinEditorStrings.cs +++ b/osu.Game/Localisation/SkinEditorStrings.cs @@ -42,7 +42,12 @@ namespace osu.Game.Localisation /// /// "Currently editing" /// - public static LocalisableString CurrentlyEditing => new TranslatableString(getKey(@"currently_editing"), "Currently editing"); + public static LocalisableString CurrentlyEditing => new TranslatableString(getKey(@"currently_editing"), @"Currently editing"); + + /// + /// "All layout elements for layers in the current screen will be reset to defaults." + /// + 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}"; } diff --git a/osu.Game/Overlays/Dialog/DeleteConfirmationDialog.cs b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs similarity index 66% rename from osu.Game/Overlays/Dialog/DeleteConfirmationDialog.cs rename to osu.Game/Overlays/Dialog/DangerousActionDialog.cs index ddb59c4c9e..c86570386f 100644 --- a/osu.Game/Overlays/Dialog/DeleteConfirmationDialog.cs +++ b/osu.Game/Overlays/Dialog/DangerousActionDialog.cs @@ -8,18 +8,22 @@ using osu.Game.Localisation; namespace osu.Game.Overlays.Dialog { /// - /// Base class for various confirmation dialogs that concern deletion actions. + /// A dialog which provides confirmation for actions which result in permanent consequences. /// Differs from in that the confirmation button is a "dangerous" one /// (requires the confirm button to be held). /// - public abstract partial class DeleteConfirmationDialog : PopupDialog + /// + /// The default implementation comes with text for a generic deletion operation. + /// This can be further customised by specifying custom . + /// + public abstract partial class DangerousActionDialog : PopupDialog { /// /// The action which performs the deletion. /// - 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 { diff --git a/osu.Game/Overlays/Mods/DeleteModPresetDialog.cs b/osu.Game/Overlays/Mods/DeleteModPresetDialog.cs index 800ebe8b4e..9788764453 100644 --- a/osu.Game/Overlays/Mods/DeleteModPresetDialog.cs +++ b/osu.Game/Overlays/Mods/DeleteModPresetDialog.cs @@ -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) { BodyText = modPreset.PerformRead(preset => preset.Name); - DeleteAction = () => modPreset.PerformWrite(preset => preset.DeletePending = true); + DangerousAction = () => modPreset.PerformWrite(preset => preset.DeletePending = true); } } } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs index 948d646e3d..99ef62d94b 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs @@ -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; } } } diff --git a/osu.Game/Overlays/Settings/SettingsPercentageSlider.cs b/osu.Game/Overlays/Settings/SettingsPercentageSlider.cs new file mode 100644 index 0000000000..fa59d18de1 --- /dev/null +++ b/osu.Game/Overlays/Settings/SettingsPercentageSlider.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . 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 +{ + /// + /// A that displays its value as a percentage by default. + /// Mostly provided for convenience of use with . + /// + public partial class SettingsPercentageSlider : SettingsSlider + where TValue : struct, IEquatable, IComparable, IConvertible + { + protected override Drawable CreateControl() => ((RoundedSliderBar)base.CreateControl()).With(sliderBar => sliderBar.DisplayAsPercentage = true); + } +} diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 49cacf9498..2b23ce290f 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -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()), }, @@ -670,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 diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiNoticeContainer.cs b/osu.Game/Overlays/Wiki/Markdown/WikiNoticeContainer.cs index 5f8bee7558..a40bd14878 100644 --- a/osu.Game/Overlays/Wiki/Markdown/WikiNoticeContainer.cs +++ b/osu.Game/Overlays/Wiki/Markdown/WikiNoticeContainer.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Resources.Localisation.Web; +using osuTK; namespace osu.Game.Overlays.Wiki.Markdown { @@ -19,24 +20,30 @@ namespace osu.Game.Overlays.Wiki.Markdown { private readonly bool isOutdated; private readonly bool needsCleanup; + private readonly bool isStub; public WikiNoticeContainer(YamlFrontMatterBlock yamlFrontMatterBlock) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; Direction = FillDirection.Vertical; + Spacing = new Vector2(10); foreach (object line in yamlFrontMatterBlock.Lines) { switch (line.ToString()) { - case "outdated: true": + case @"outdated: true": isOutdated = true; break; - case "needs_cleanup: true": + case @"needs_cleanup: true": needsCleanup = true; break; + + case @"stub: true": + isStub = true; + break; } } } @@ -60,6 +67,14 @@ namespace osu.Game.Overlays.Wiki.Markdown Text = WikiStrings.ShowNeedsCleanupOrRewrite, }); } + + if (isStub) + { + Add(new NoticeBox + { + Text = WikiStrings.ShowStub, + }); + } } private partial class NoticeBox : Container diff --git a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs index dc7594f469..d4223a80c2 100644 --- a/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs +++ b/osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs @@ -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))] + [SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(SettingsPercentageSlider))] public BindableNumber 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 - { - public PercentSlider() - { - DisplayAsPercentage = true; - } - } } diff --git a/osu.Game/Rulesets/Objects/BarLineGenerator.cs b/osu.Game/Rulesets/Objects/BarLineGenerator.cs index af32c7def3..affbcbd878 100644 --- a/osu.Game/Rulesets/Objects/BarLineGenerator.cs +++ b/osu.Game/Rulesets/Objects/BarLineGenerator.cs @@ -38,7 +38,6 @@ namespace osu.Game.Rulesets.Objects for (int i = 0; i < timingPoints.Count; i++) { TimingControlPoint currentTimingPoint = timingPoints[i]; - EffectControlPoint currentEffectPoint = beatmap.ControlPointInfo.EffectPointAt(currentTimingPoint.Time); int currentBeat = 0; // Don't generate barlines before the hit object or t=0 (whichever is earliest). Some beatmaps use very unrealistic values here (although none are ranked). @@ -66,7 +65,7 @@ namespace osu.Game.Rulesets.Objects startTime = currentTimingPoint.Time + barCount * barLength; } - if (currentEffectPoint.OmitFirstBarLine) + if (currentTimingPoint.OmitFirstBarLine) { startTime += barLength; } diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index a5e442b7de..7bf0482673 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -39,6 +39,9 @@ namespace osu.Game.Rulesets.UI { set { + if (value == recorder) + return; + if (value != null && recorder != null) throw new InvalidOperationException("Cannot attach more than one recorder"); diff --git a/osu.Game/Screens/Edit/DeleteDifficultyConfirmationDialog.cs b/osu.Game/Screens/Edit/DeleteDifficultyConfirmationDialog.cs index 68a0ef4250..8556949528 100644 --- a/osu.Game/Screens/Edit/DeleteDifficultyConfirmationDialog.cs +++ b/osu.Game/Screens/Edit/DeleteDifficultyConfirmationDialog.cs @@ -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; } } } diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 08b2ce8562..b078e3fa44 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private EditorClock clock { get; set; } = null!; - public const float TIMING_COLUMN_WIDTH = 230; + public const float TIMING_COLUMN_WIDTH = 300; public IEnumerable ControlGroups { diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs index 051ac97366..7e484433f7 100644 --- a/osu.Game/Screens/Edit/Timing/EffectSection.cs +++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs @@ -14,7 +14,6 @@ namespace osu.Game.Screens.Edit.Timing internal partial class EffectSection : Section { private LabelledSwitchButton kiai = null!; - private LabelledSwitchButton omitBarLine = null!; private SliderWithTextBoxInput scrollSpeedSlider = null!; @@ -24,7 +23,6 @@ namespace osu.Game.Screens.Edit.Timing Flow.AddRange(new Drawable[] { kiai = new LabelledSwitchButton { Label = "Kiai Time" }, - omitBarLine = new LabelledSwitchButton { Label = "Skip Bar Line" }, scrollSpeedSlider = new SliderWithTextBoxInput("Scroll Speed") { Current = new EffectControlPoint().ScrollSpeedBindable, @@ -38,7 +36,6 @@ namespace osu.Game.Screens.Edit.Timing base.LoadComplete(); kiai.Current.BindValueChanged(_ => saveChanges()); - omitBarLine.Current.BindValueChanged(_ => saveChanges()); scrollSpeedSlider.Current.BindValueChanged(_ => saveChanges()); var drawableRuleset = Beatmap.BeatmapInfo.Ruleset.CreateInstance().CreateDrawableRulesetWith(Beatmap.PlayableBeatmap); @@ -60,7 +57,6 @@ namespace osu.Game.Screens.Edit.Timing isRebinding = true; kiai.Current = point.NewValue.KiaiModeBindable; - omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable; scrollSpeedSlider.Current = point.NewValue.ScrollSpeedBindable; isRebinding = false; @@ -74,7 +70,6 @@ namespace osu.Game.Screens.Edit.Timing return new EffectControlPoint { KiaiMode = reference.KiaiMode, - OmitFirstBarLine = reference.OmitFirstBarLine, ScrollSpeed = reference.ScrollSpeed, }; } diff --git a/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs index 3b068699ca..ad22aa81fc 100644 --- a/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs +++ b/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs @@ -11,18 +11,15 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes public partial class EffectRowAttribute : RowAttribute { private readonly Bindable kiaiMode; - private readonly Bindable omitBarLine; private readonly BindableNumber scrollSpeed; private AttributeText kiaiModeBubble = null!; - private AttributeText omitBarLineBubble = null!; private AttributeText text = null!; public EffectRowAttribute(EffectControlPoint effect) : base(effect, "effect") { kiaiMode = effect.KiaiModeBindable.GetBoundCopy(); - omitBarLine = effect.OmitFirstBarLineBindable.GetBoundCopy(); scrollSpeed = effect.ScrollSpeedBindable.GetBoundCopy(); } @@ -37,11 +34,9 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes }, text = new AttributeText(Point) { Width = 45 }, kiaiModeBubble = new AttributeText(Point) { Text = "kiai" }, - omitBarLineBubble = new AttributeText(Point) { Text = "no barline" }, }); kiaiMode.BindValueChanged(enabled => kiaiModeBubble.FadeTo(enabled.NewValue ? 1 : 0), true); - omitBarLine.BindValueChanged(enabled => omitBarLineBubble.FadeTo(enabled.NewValue ? 1 : 0), true); scrollSpeed.BindValueChanged(_ => updateText(), true); } diff --git a/osu.Game/Screens/Edit/Timing/RowAttributes/TimingRowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttributes/TimingRowAttribute.cs index a6d3816bd4..577e7a3134 100644 --- a/osu.Game/Screens/Edit/Timing/RowAttributes/TimingRowAttribute.cs +++ b/osu.Game/Screens/Edit/Timing/RowAttributes/TimingRowAttribute.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Graphics; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; using osu.Game.Graphics.Sprites; @@ -14,24 +15,32 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes public partial class TimingRowAttribute : RowAttribute { private readonly BindableNumber beatLength; + private readonly Bindable omitBarLine; private readonly Bindable timeSignature; + private AttributeText omitBarLineBubble = null!; private OsuSpriteText text = null!; public TimingRowAttribute(TimingControlPoint timing) : base(timing, "timing") { timeSignature = timing.TimeSignatureBindable.GetBoundCopy(); + omitBarLine = timing.OmitFirstBarLineBindable.GetBoundCopy(); beatLength = timing.BeatLengthBindable.GetBoundCopy(); } [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - Content.Add(text = new AttributeText(Point)); + Content.AddRange(new[] + { + text = new AttributeText(Point), + omitBarLineBubble = new AttributeText(Point) { Text = "no barline" }, + }); Background.Colour = colourProvider.Background4; timeSignature.BindValueChanged(_ => updateText()); + omitBarLine.BindValueChanged(enabled => omitBarLineBubble.FadeTo(enabled.NewValue ? 1 : 0), true); beatLength.BindValueChanged(_ => updateText(), true); } diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index d2d524ccbe..2757753b07 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -12,6 +12,7 @@ namespace osu.Game.Screens.Edit.Timing internal partial class TimingSection : Section { private LabelledTimeSignature timeSignature = null!; + private LabelledSwitchButton omitBarLine = null!; private BPMTextBox bpmTextEntry = null!; [BackgroundDependencyLoader] @@ -24,7 +25,8 @@ namespace osu.Game.Screens.Edit.Timing timeSignature = new LabelledTimeSignature { Label = "Time Signature" - } + }, + omitBarLine = new LabelledSwitchButton { Label = "Skip Bar Line" }, }); } @@ -33,6 +35,7 @@ namespace osu.Game.Screens.Edit.Timing base.LoadComplete(); bpmTextEntry.Current.BindValueChanged(_ => saveChanges()); + omitBarLine.Current.BindValueChanged(_ => saveChanges()); timeSignature.Current.BindValueChanged(_ => saveChanges()); void saveChanges() @@ -51,6 +54,7 @@ namespace osu.Game.Screens.Edit.Timing bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable; timeSignature.Current = point.NewValue.TimeSignatureBindable; + omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable; isRebinding = false; } @@ -63,7 +67,8 @@ namespace osu.Game.Screens.Edit.Timing return new TimingControlPoint { BeatLength = reference.BeatLength, - TimeSignature = reference.TimeSignature + TimeSignature = reference.TimeSignature, + OmitFirstBarLine = reference.OmitFirstBarLine, }; } diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs index 06509b6465..a152f4be19 100644 --- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs +++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs @@ -114,7 +114,7 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, FillMode = FillMode.Fill, }, - loading = new LoadingLayer(true) + loading = new LoadingLayer(dimBackground: true, blockInput: false) } }, versionFlow = new FillFlowContainer diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs index 6db1072fbb..dd6e10ba5d 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs @@ -242,7 +242,6 @@ namespace osu.Game.Screens.Play.HUD { length = value; mask.Width = value * DrawWidth; - fill.Width = value * DrawWidth; } } diff --git a/osu.Game/Screens/Play/HUD/PlayerAvatar.cs b/osu.Game/Screens/Play/HUD/PlayerAvatar.cs new file mode 100644 index 0000000000..1d0331593a --- /dev/null +++ b/osu.Game/Screens/Play/HUD/PlayerAvatar.cs @@ -0,0 +1,58 @@ +// Copyright (c) ppy Pty Ltd . 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))] + 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; } + } +} diff --git a/osu.Game/Screens/Play/HUD/PlayerFlag.cs b/osu.Game/Screens/Play/HUD/PlayerFlag.cs new file mode 100644 index 0000000000..85799c03d3 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/PlayerFlag.cs @@ -0,0 +1,36 @@ +// Copyright (c) ppy Pty Ltd . 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; } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 6dc4854e80..bc453d2151 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -276,7 +276,7 @@ namespace osu.Game.Screens.Play }, FailOverlay = new FailOverlay { - SaveReplay = prepareAndImportScore, + SaveReplay = async () => await prepareAndImportScoreAsync(true).ConfigureAwait(false), OnRetry = () => Restart(), OnQuit = () => PerformExit(true), }, @@ -613,6 +613,9 @@ namespace osu.Game.Screens.Play // if an exit has been requested, cancel any pending completion (the user has shown intention to exit). resultsDisplayDelegate?.Cancel(); + // import current score if possible. + prepareAndImportScoreAsync(); + // The actual exit is performed if // - the pause / fail dialog was not requested // - the pause / fail dialog was requested but is already displayed (user showing intention to exit). @@ -735,14 +738,9 @@ namespace osu.Game.Screens.Play // is no chance that a user could return to the (already completed) Player instance from a child screen. ValidForResume = false; - // Ensure we are not writing to the replay any more, as we are about to consume and store the score. - DrawableRuleset.SetRecordTarget(null); - if (!Configuration.ShowResults) return; - prepareScoreForDisplayTask ??= Task.Run(prepareAndImportScore); - bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value; if (storyboardHasOutro) @@ -756,35 +754,6 @@ namespace osu.Game.Screens.Play progressToResults(true); } - /// - /// Asynchronously run score preparation operations (database import, online submission etc.). - /// - /// The final score. - private async Task prepareAndImportScore() - { - var scoreCopy = Score.DeepClone(); - - try - { - await PrepareScoreForResultsAsync(scoreCopy).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.Error(ex, @"Score preparation failed!"); - } - - try - { - await ImportScore(scoreCopy).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.Error(ex, @"Score import failed!"); - } - - return scoreCopy.ScoreInfo; - } - /// /// Queue the results screen for display. /// @@ -800,12 +769,25 @@ namespace osu.Game.Screens.Play resultsDisplayDelegate = new ScheduledDelegate(() => { - if (prepareScoreForDisplayTask?.IsCompleted != true) + if (prepareScoreForDisplayTask == null) + { + // Try importing score since the task hasn't been invoked yet. + prepareAndImportScoreAsync(); + return; + } + + if (!prepareScoreForDisplayTask.IsCompleted) // If the asynchronous preparation has not completed, keep repeating this delegate. return; resultsDisplayDelegate?.Cancel(); + if (prepareScoreForDisplayTask.GetResultSafely() == null) + { + // If score import did not occur, we do not want to show the results screen. + return; + } + if (!this.IsCurrentScreen()) // This player instance may already be in the process of exiting. return; @@ -816,6 +798,51 @@ namespace osu.Game.Screens.Play Scheduler.Add(resultsDisplayDelegate); } + /// + /// Asynchronously run score preparation operations (database import, online submission etc.). + /// + /// Whether the score should be imported even if non-passing (or the current configuration doesn't allow for it). + /// The final score. + [ItemCanBeNull] + private Task prepareAndImportScoreAsync(bool forceImport = false) + { + // Ensure we are not writing to the replay any more, as we are about to consume and store the score. + DrawableRuleset.SetRecordTarget(null); + + if (prepareScoreForDisplayTask != null) + return prepareScoreForDisplayTask; + + // We do not want to import the score in cases where we don't show results + bool canShowResults = Configuration.ShowResults && ScoreProcessor.HasCompleted.Value && GameplayState.HasPassed; + if (!canShowResults && !forceImport) + return Task.FromResult(null); + + return prepareScoreForDisplayTask = Task.Run(async () => + { + var scoreCopy = Score.DeepClone(); + + try + { + await PrepareScoreForResultsAsync(scoreCopy).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.Error(ex, @"Score preparation failed!"); + } + + try + { + await ImportScore(scoreCopy).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.Error(ex, @"Score import failed!"); + } + + return scoreCopy.ScoreInfo; + }); + } + protected override bool OnScroll(ScrollEvent e) { // During pause, allow global volume adjust regardless of settings. diff --git a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs index c0f97a05e2..8efad451df 100644 --- a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs +++ b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs @@ -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); diff --git a/osu.Game/Screens/Select/BeatmapDeleteDialog.cs b/osu.Game/Screens/Select/BeatmapDeleteDialog.cs index 4ab23c3a3a..e98af8cca2 100644 --- a/osu.Game/Screens/Select/BeatmapDeleteDialog.cs +++ b/osu.Game/Screens/Select/BeatmapDeleteDialog.cs @@ -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); } } } diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 837939716b..7e48bc5cdd 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -26,6 +26,11 @@ namespace osu.Game.Screens.Select.Carousel { base.Filter(criteria); + Filtered.Value = !checkMatch(criteria); + } + + private bool checkMatch(FilterCriteria criteria) + { bool match = criteria.Ruleset == null || BeatmapInfo.Ruleset.ShortName == criteria.Ruleset.ShortName || @@ -34,8 +39,7 @@ namespace osu.Game.Screens.Select.Carousel if (BeatmapInfo.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) { // only check ruleset equality or convertability for selected beatmap - Filtered.Value = !match; - return; + return match; } match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(BeatmapInfo.StarRating); @@ -49,18 +53,38 @@ namespace osu.Game.Screens.Select.Carousel match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(BeatmapInfo.BeatDivisor); match &= !criteria.OnlineStatus.HasFilter || criteria.OnlineStatus.IsInRange(BeatmapInfo.Status); + if (!match) return false; + match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(BeatmapInfo.Metadata.Author.Username); match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) || criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode); match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating); - if (match && criteria.SearchTerms.Length > 0) + if (!match) return false; + + if (criteria.SearchTerms.Length > 0) { - string[] terms = BeatmapInfo.GetSearchableTerms(); + var terms = BeatmapInfo.GetSearchableTerms(); foreach (string criteriaTerm in criteria.SearchTerms) - match &= terms.Any(term => term.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase)); + { + bool any = false; + + // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator + foreach (string term in terms) + { + if (!term.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase)) continue; + + any = true; + break; + } + + if (any) continue; + + match = false; + break; + } // if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs. // this should be done after text matching so we can prioritise matching numbers in metadata. @@ -71,13 +95,14 @@ namespace osu.Game.Screens.Select.Carousel } } - if (match) - match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true; + if (!match) return false; + + match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true; if (match && criteria.RulesetCriteria != null) match &= criteria.RulesetCriteria.Matches(BeatmapInfo); - Filtered.Value = !match; + return match; } public override int CompareTo(FilterCriteria criteria, CarouselItem other) diff --git a/osu.Game/Screens/Select/Carousel/UpdateLocalConfirmationDialog.cs b/osu.Game/Screens/Select/Carousel/UpdateLocalConfirmationDialog.cs index e1aa662942..6157e8f6a5 100644 --- a/osu.Game/Screens/Select/Carousel/UpdateLocalConfirmationDialog.cs +++ b/osu.Game/Screens/Select/Carousel/UpdateLocalConfirmationDialog.cs @@ -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; } } } diff --git a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs index 6349e9e5eb..c4add31a4f 100644 --- a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs +++ b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs @@ -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); } } } diff --git a/osu.Game/Screens/Select/SkinDeleteDialog.cs b/osu.Game/Screens/Select/SkinDeleteDialog.cs index 9e11629890..6612ae837a 100644 --- a/osu.Game/Screens/Select/SkinDeleteDialog.cs +++ b/osu.Game/Screens/Select/SkinDeleteDialog.cs @@ -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(); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index f4b0692619..eec2cd6a60 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -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); } } } diff --git a/osu.Game/Users/Drawables/UpdateableFlag.cs b/osu.Game/Users/Drawables/UpdateableFlag.cs index a208f3c7c4..8f8d7052e5 100644 --- a/osu.Game/Users/Drawables/UpdateableFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableFlag.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . 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). /// - 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) { diff --git a/osu.Game/Users/UserListPanel.cs b/osu.Game/Users/UserListPanel.cs index bbd3c60a33..3047e70a1a 100644 --- a/osu.Game/Users/UserListPanel.cs +++ b/osu.Game/Users/UserListPanel.cs @@ -67,6 +67,7 @@ namespace osu.Game.Users { username.Anchor = Anchor.CentreLeft; username.Origin = Anchor.CentreLeft; + username.UseFullGlyphHeight = false; }) } }, @@ -95,13 +96,23 @@ namespace osu.Game.Users } }; + if (User.Groups != null) + { + details.Add(new GroupBadgeFlow + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + User = { Value = User } + }); + } + if (User.IsSupporter) { details.Add(new SupporterIcon { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Height = 20, + Height = 16, SupportLevel = User.SupportLevel }); }