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

Merge branch 'master' into improve-alternate-after-break

This commit is contained in:
Bartłomiej Dach 2022-05-02 10:52:48 +02:00 committed by GitHub
commit bba7722837
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 1483 additions and 108 deletions

View File

@ -191,4 +191,5 @@ dotnet_diagnostic.CA2225.severity = none
# Banned APIs
dotnet_diagnostic.RS0030.severity = error
dotnet_diagnostic.OLOC001.words_in_name = 5
dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text.

View File

@ -52,7 +52,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.422.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.428.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.430.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>
{
public override Type[] IncompatibleMods => new[] { typeof(OsuModStrictTracking) };
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModStrictTracking)).ToArray();
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);

View File

@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Description => "It never gets boring!";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTarget)).ToArray();
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
private Random? rng;

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation;
public override string Description => @"Spinners will be automatically completed.";
public override double ScoreMultiplier => 0.9;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) };
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTarget) };
public void ApplyToDrawableHitObject(DrawableHitObject hitObject)
{

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.DifficultyIncrease;
public override string Description => @"Follow circles just got serious...";
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModClassic) };
public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTarget) };
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{

View File

@ -42,7 +42,14 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => @"Practice keeping up with the beat of the song.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSuddenDeath) };
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
{
typeof(IRequiresApproachCircles),
typeof(OsuModRandom),
typeof(OsuModSpunOut),
typeof(OsuModStrictTracking),
typeof(OsuModSuddenDeath)
}).ToArray();
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
public Bindable<int?> Seed { get; } = new Bindable<int?>

View File

@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Osu.UI
},
new SettingsCheckbox
{
ClassicDefault = false,
LabelText = "Snaking out sliders",
Current = config.GetBindable<bool>(OsuRulesetSetting.SnakingOutSliders)
},

View File

@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
AddUntilStep("wait for player to be current", () => player.IsCurrentScreen() && player.IsLoaded);
AddStep("start gameplay", () => ((IMultiplayerClient)MultiplayerClient).MatchStarted());
AddStep("start gameplay", () => ((IMultiplayerClient)MultiplayerClient).GameplayStarted());
}
[Test]

View File

@ -0,0 +1,24 @@
// 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 NUnit.Framework;
using osu.Game.Beatmaps.Drawables;
namespace osu.Game.Tests.Visual.Online
{
[Ignore("Only for visual testing")]
public class TestSceneBundledBeatmapDownloader : OsuTestScene
{
private BundledBeatmapDownloader downloader;
[Test]
public void TestDownloader()
{
AddStep("Create downloader", () =>
{
downloader?.Expire();
Add(downloader = new BundledBeatmapDownloader(false));
});
}
}
}

View File

@ -1,35 +1,99 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Screens.Select;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
{
public class TestSceneSongSelectFooter : OsuManualInputManagerTestScene
{
public TestSceneSongSelectFooter()
{
AddStep("Create footer", () =>
{
Footer footer;
AddRange(new Drawable[]
{
footer = new Footer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
});
private FooterButtonRandom randomButton;
footer.AddButton(new FooterButtonMods(), null);
footer.AddButton(new FooterButtonRandom
{
NextRandom = () => { },
PreviousRandom = () => { },
}, null);
footer.AddButton(new FooterButtonOptions(), null);
private bool nextRandomCalled;
private bool previousRandomCalled;
[SetUp]
public void SetUp() => Schedule(() =>
{
nextRandomCalled = false;
previousRandomCalled = false;
Footer footer;
Child = footer = new Footer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
footer.AddButton(new FooterButtonMods(), null);
footer.AddButton(randomButton = new FooterButtonRandom
{
NextRandom = () => nextRandomCalled = true,
PreviousRandom = () => previousRandomCalled = true,
}, null);
footer.AddButton(new FooterButtonOptions(), null);
InputManager.MoveMouseTo(Vector2.Zero);
});
[Test]
public void TestFooterRandom()
{
AddStep("press F2", () => InputManager.Key(Key.F2));
AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
}
[Test]
public void TestFooterRandomViaMouse()
{
AddStep("click button", () =>
{
InputManager.MoveMouseTo(randomButton);
InputManager.Click(MouseButton.Left);
});
AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
}
[Test]
public void TestFooterRewind()
{
AddStep("press Shift+F2", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.PressKey(Key.F2);
InputManager.ReleaseKey(Key.F2);
InputManager.ReleaseKey(Key.LShift);
});
AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
}
[Test]
public void TestFooterRewindViaShiftMouseLeft()
{
AddStep("shift + click button", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.MoveMouseTo(randomButton);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.LShift);
});
AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
}
[Test]
public void TestFooterRewindViaMouseRight()
{
AddStep("right click button", () =>
{
InputManager.MoveMouseTo(randomButton);
InputManager.Click(MouseButton.Right);
});
AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
}
}
}

View File

@ -0,0 +1,24 @@
// 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.Screens;
using osu.Game.Overlays;
using osu.Game.Overlays.FirstRunSetup;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneFirstRunScreenBehaviour : OsuManualInputManagerTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
public TestSceneFirstRunScreenBehaviour()
{
AddStep("load screen", () =>
{
Child = new ScreenStack(new ScreenBehaviour());
});
}
}
}

View File

@ -0,0 +1,24 @@
// 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.Screens;
using osu.Game.Overlays;
using osu.Game.Overlays.FirstRunSetup;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneFirstRunScreenBundledBeatmaps : OsuManualInputManagerTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
public TestSceneFirstRunScreenBundledBeatmaps()
{
AddStep("load screen", () =>
{
Child = new ScreenStack(new ScreenBeatmaps());
});
}
}
}

View File

@ -65,6 +65,12 @@ namespace osu.Game.Tests.Visual.UserInterface
});
}
[Test]
public void TestBasic()
{
AddAssert("overlay visible", () => overlay.State.Value == Visibility.Visible);
}
[Test]
[Ignore("Enable when first run setup is being displayed on first run.")]
public void TestDoesntOpenOnSecondRun()
@ -178,7 +184,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("step to next", () => overlay.NextButton.TriggerClick());
AddAssert("is at known screen", () => overlay.CurrentScreen is ScreenUIScale);
AddAssert("is at known screen", () => overlay.CurrentScreen is ScreenBeatmaps);
AddStep("hide", () => overlay.Hide());
AddAssert("overlay hidden", () => overlay.State.Value == Visibility.Hidden);
@ -188,7 +194,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("run notification action", () => lastNotification.Activated());
AddAssert("overlay shown", () => overlay.State.Value == Visibility.Visible);
AddAssert("is resumed", () => overlay.CurrentScreen is ScreenUIScale);
AddAssert("is resumed", () => overlay.CurrentScreen is ScreenBeatmaps);
}
}
}

View File

@ -111,6 +111,33 @@ namespace osu.Game.Tests.Visual.UserInterface
&& SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModMirror)));
}
[Test]
public void TestDimmedState()
{
createScreen();
changeRuleset(0);
AddUntilStep("any column dimmed", () => this.ChildrenOfType<ModColumn>().Any(column => !column.Active.Value));
ModColumn lastColumn = null;
AddAssert("last column dimmed", () => !this.ChildrenOfType<ModColumn>().Last().Active.Value);
AddStep("request scroll to last column", () =>
{
var lastDimContainer = this.ChildrenOfType<ModSelectScreen.ColumnDimContainer>().Last();
lastColumn = lastDimContainer.Column;
lastDimContainer.RequestScroll?.Invoke(lastDimContainer);
});
AddUntilStep("column undimmed", () => lastColumn.Active.Value);
AddStep("click panel", () =>
{
InputManager.MoveMouseTo(lastColumn.ChildrenOfType<ModPanel>().First());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("panel selected", () => lastColumn.ChildrenOfType<ModPanel>().First().Active.Value);
}
[Test]
public void TestCustomisationToggleState()
{

View File

@ -1,3 +1,4 @@
[*.cs]
dotnet_diagnostic.OLOC001.words_in_name = 5
dotnet_diagnostic.OLOC001.prefix_namespace = osu.Game.Resources.Localisation
dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text.
dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text.

View File

@ -0,0 +1,339 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Game.Database;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Utils;
namespace osu.Game.Beatmaps.Drawables
{
public class BundledBeatmapDownloader : CompositeDrawable
{
private readonly bool shouldPostNotifications;
public IEnumerable<BeatmapDownloadTracker> DownloadTrackers => downloadTrackers;
private readonly List<BeatmapDownloadTracker> downloadTrackers = new List<BeatmapDownloadTracker>();
private readonly List<string> downloadableFilenames = new List<string>();
private BundledBeatmapModelDownloader beatmapDownloader;
/// <summary>
/// Construct a new beatmap downloader.
/// </summary>
/// <param name="onlyTutorial">Whether only the tutorial should be downloaded, instead of bundled beatmaps.</param>
/// <param name="shouldPostNotifications">Whether downloads should create tracking notifications.</param>
public BundledBeatmapDownloader(bool onlyTutorial, bool shouldPostNotifications = false)
{
this.shouldPostNotifications = shouldPostNotifications;
if (onlyTutorial)
{
queueDownloads(new[] { tutorial_filename });
}
else
{
queueDownloads(always_bundled_beatmaps);
queueDownloads(bundled_osu, 8);
queueDownloads(bundled_taiko, 3);
queueDownloads(bundled_catch, 3);
queueDownloads(bundled_mania, 3);
}
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var localDependencies = new DependencyContainer(base.CreateChildDependencies(parent));
localDependencies.CacheAs<BeatmapModelDownloader>(beatmapDownloader = new BundledBeatmapModelDownloader(parent.Get<BeatmapManager>(), parent.Get<IAPIProvider>()));
if (shouldPostNotifications && parent.Get<INotificationOverlay>() is INotificationOverlay notifications)
beatmapDownloader.PostNotification = notifications.Post;
return localDependencies;
}
[BackgroundDependencyLoader]
private void load()
{
foreach (string filename in downloadableFilenames)
{
var match = Regex.Match(filename, @"([0-9]*) (.*) - (.*)\.osz");
var beatmapSet = new APIBeatmapSet
{
OnlineID = int.Parse(match.Groups[1].Value),
Artist = match.Groups[2].Value,
Title = match.Groups[3].Value,
};
var beatmapDownloadTracker = new BeatmapDownloadTracker(beatmapSet);
downloadTrackers.Add(beatmapDownloadTracker);
AddInternal(beatmapDownloadTracker);
beatmapDownloader.Download(beatmapSet);
}
}
private void queueDownloads(string[] sourceFilenames, int? limit = null)
{
try
{
// Matches osu-stable, in order to provide new users with roughly the same randomised selection of bundled beatmaps.
var random = new LegacyRandom(DateTime.UtcNow.Year * 1000 + (DateTime.UtcNow.DayOfYear / 7));
downloadableFilenames.AddRange(sourceFilenames.OrderBy(x => random.NextDouble()).Take(limit ?? int.MaxValue));
}
catch { }
}
private class BundledBeatmapModelDownloader : BeatmapModelDownloader
{
public BundledBeatmapModelDownloader(IModelImporter<BeatmapSetInfo> beatmapImporter, IAPIProvider api)
: base(beatmapImporter, api)
{
}
protected override ArchiveDownloadRequest<IBeatmapSetInfo> CreateDownloadRequest(IBeatmapSetInfo set, bool minimiseDownloadSize)
=> new BundledBeatmapDownloadRequest(set, minimiseDownloadSize);
public class BundledBeatmapDownloadRequest : DownloadBeatmapSetRequest
{
protected override string Uri => $"https://assets.ppy.sh/client-resources/bundled/{Model.OnlineID}.osz";
public BundledBeatmapDownloadRequest(IBeatmapSetInfo beatmapSetInfo, bool minimiseDownloadSize)
: base(beatmapSetInfo, minimiseDownloadSize)
{
}
}
}
private const string tutorial_filename = "1011011 nekodex - new beginnings.osz";
/// <summary>
/// Contest winners or other special cases.
/// </summary>
private static readonly string[] always_bundled_beatmaps =
{
// This thing is 40mb, I'm not sure we want it here...
@"1388906 Raphlesia & BilliumMoto - My Love.osz"
};
private static readonly string[] bundled_osu =
{
"682286 Yuyoyuppe - Emerald Galaxy.osz",
"682287 baker - For a Dead Girl+.osz",
"682289 Hige Driver - I Wanna Feel Your Love (feat. shully).osz",
"682290 Hige Driver - Miracle Sugite Yabai (feat. shully).osz",
"682416 Hige Driver - Palette.osz",
"682595 baker - Kimi ga Kimi ga -vocanico remix-.osz",
"716211 yuki. - Spring Signal.osz",
"716213 dark cat - BUBBLE TEA (feat. juu & cinders).osz",
"716215 LukHash - CLONED.osz",
"716219 IAHN - Snowdrop.osz",
"716249 *namirin - Senaka Awase no Kuukyo (with Kakichoco).osz",
"716390 sakuraburst - SHA.osz",
"716441 Fractal Dreamers - Paradigm Shift.osz",
"729808 Thaehan - Leprechaun.osz",
"751771 Cranky - Hanaarashi.osz",
"751772 Cranky - Ran.osz",
"751773 Cranky - Feline, the White....osz",
"751774 Function Phantom - Variable.osz",
"751779 Rin - Daishibyo set 14 ~ Sado no Futatsuiwa.osz",
"751782 Fractal Dreamers - Fata Morgana.osz",
"751785 Cranky - Chandelier - King.osz",
"751846 Fractal Dreamers - Celestial Horizon.osz",
"751866 Rin - Moriya set 08 ReEdit ~ Youkai no Yama.osz",
"751894 Fractal Dreamers - Blue Haven.osz",
"751896 Cranky - Rave 2 Rave.osz",
"751932 Cranky - La fuite des jours.osz",
"751972 Cranky - CHASER.osz",
"779173 Thaehan - Superpower.osz",
"780932 VINXIS - A Centralized View.osz",
"785572 S3RL - I'll See You Again (feat. Chi Chi).osz",
"785650 yuki. feat. setsunan - Hello! World.osz",
"785677 Dictate - Militant.osz",
"785731 S3RL - Catchit (Radio Edit).osz",
"785774 LukHash - GLITCH.osz",
"786498 Trial & Error - Tokoyami no keiyaku KEGARETA-SHOUJO feat. GUMI.osz",
"789374 Pulse - LP.osz",
"789528 James Portland - Sky.osz",
"789529 Lexurus - Gravity.osz",
"789544 Andromedik - Invasion.osz",
"789905 Gourski x Himmes - Silence.osz",
"791667 cYsmix - Babaroque (Short Ver.).osz",
"791798 cYsmix - Behind the Walls.osz",
"791845 cYsmix - Little Knight.osz",
"792241 cYsmix - Eden.osz",
"792396 cYsmix - The Ballad of a Mindless Girl.osz",
"795432 Phonetic - Journey.osz",
"831322 DJ'TEKINA//SOMETHING - Hidamari no Uta.osz",
"847764 Cranky - Crocus.osz",
"847776 Culprate & Joe Ford - Gaucho.osz",
"847812 J. Pachelbel - Canon (Cranky Remix).osz",
"847900 Cranky - Time Alter.osz",
"847930 LukHash - 8BIT FAIRY TALE.osz",
"848003 Culprate - Aurora.osz",
"848068 nanobii - popsicle beach.osz",
"848090 Trial & Error - DAI*TAN SENSATION feat. Nanahira, Mii, Aitsuki Nakuru (Short Ver.).osz",
"848259 Culprate & Skorpion - Jester.osz",
"848976 Dictate - Treason.osz",
"851543 Culprate - Florn.osz",
"864748 Thaehan - Angry Birds Epic (Remix).osz",
"873667 OISHII - ONIGIRI FREEWAY.osz",
"876227 Culprate, Keota & Sophie Meiers - Mechanic Heartbeat.osz",
"880487 cYsmix - Peer Gynt.osz",
"883088 Wisp X - Somewhere I'd Rather Be.osz",
"891333 HyuN - White Aura.osz",
"891334 HyuN - Wild Card.osz",
"891337 HyuN feat. LyuU - Cross Over.osz",
"891338 HyuN & Ritoru - Apocalypse in Love.osz",
"891339 HyuN feat. Ato - Asu wa Ame ga Yamukara.osz",
"891345 HyuN - Infinity Heaven.osz",
"891348 HyuN - Guitian.osz",
"891356 HyuN - Legend of Genesis.osz",
"891366 HyuN - Illusion of Inflict.osz",
"891417 HyuN feat. Yu-A - My life is for you.osz",
"891441 HyuN - You'Re aRleAdY dEAd.osz",
"891632 HyuN feat. YURI - Disorder.osz",
"891712 HyuN - Tokyo's Starlight.osz",
"901091 *namirin - Ciel etoile.osz",
"916990 *namirin - Koishiteiku Planet.osz",
"929284 tieff - Sense of Nostalgia.osz",
"933940 Ben Briggs - Yes (Maybe).osz",
"934415 Ben Briggs - Fearless Living.osz",
"934627 Ben Briggs - New Game Plus.osz",
"934666 Ben Briggs - Wave Island.osz",
"936126 siromaru + cranky - conflict.osz",
"940377 onumi - ARROGANCE.osz",
"940597 tieff - Take Your Swimsuit.osz",
"941085 tieff - Our Story.osz",
"949297 tieff - Sunflower.osz",
"952380 Ben Briggs - Why Are We Yelling.osz",
"954272 *namirin - Kanzen Shouri*Esper Girl.osz",
"955866 KIRA & Heartbreaker - B.B.F (feat. Hatsune Miku & Kagamine Rin).osz",
"961320 Kuba Oms - All In All.osz",
"964553 The Flashbulb - You Take the World's Weight Away.osz",
"965651 Fractal Dreamers - Ad Astra.osz",
"966225 The Flashbulb - Passage D.osz",
"966324 DJ'TEKINA//SOMETHING - Hidamari no Uta.osz",
"972810 James Landino & Kabuki - Birdsong.osz",
"972932 James Landino - Hide And Seek.osz",
"977276 The Flashbulb - Mellann.osz",
"981616 *namirin - Mizutamari Tobikoete (with Nanahira).osz",
"985788 Loki - Wizard's Tower.osz",
"996628 OISHII - ONIGIRI FREEWAY.osz",
"996898 HyuN - White Aura.osz",
"1003554 yuki. - Nadeshiko Sensation.osz",
"1014936 Thaehan - Bwa !.osz",
"1019827 UNDEAD CORPORATION - Sad Dream.osz",
"1020213 Creo - Idolize.osz",
"1021450 Thaehan - Chiptune & Baroque.osz",
};
private static readonly string[] bundled_taiko =
{
"707824 Fractal Dreamers - Fortuna Redux.osz",
"789553 Cranky - Ran.osz",
"827822 Function Phantom - Neuronecia.osz",
"847323 Nakanojojo - Bittersweet (feat. Kuishinboakachan a.k.a Kiato).osz",
"847433 Trial & Error - Tokoyami no keiyaku KEGARETA-SHOUJO feat. GUMI.osz",
"847576 dark cat - hot chocolate.osz",
"847957 Wisp X - Final Moments.osz",
"876282 VINXIS - Greetings.osz",
"876648 Thaehan - Angry Birds Epic (Remix).osz",
"877069 IAHN - Transform (Original Mix).osz",
"877496 Thaehan - Leprechaun.osz",
"877935 Thaehan - Overpowered.osz",
"878344 yuki. - Be Your Light.osz",
"918446 VINXIS - Facade.osz",
"918903 LukHash - Ghosts.osz",
"919251 *namirin - Hitokoto no Kyori.osz",
"919704 S3RL - I Will Pick You Up (feat. Tamika).osz",
"921535 SOOOO - Raven Haven.osz",
"927206 *namirin - Kanzen Shouri*Esper Girl.osz",
"927544 Camellia feat. Nanahira - Kansoku Eisei.osz",
"930806 Nakanojojo - Pararara (feat. Amekoya).osz",
"931741 Camellia - Quaoar.osz",
"935699 Rin - Mythic set ~ Heart-Stirring Urban Legends.osz",
"935732 Thaehan - Yuujou.osz",
"941145 Function Phantom - Euclid.osz",
"942334 Dictate - Cauldron.osz",
"946540 nanobii - astral blast.osz",
"948844 Rin - Kishinjou set 01 ~ Mist Lake.osz",
"949122 Wisp X - Petal.osz",
"951618 Rin - Kishinjou set 02 ~ Mermaid from the Uncharted Land.osz",
"957412 Rin - Lunatic set 16 ~ The Space Shrine Maiden Returns Home.osz",
"961335 Thaehan - Insert Coin.osz",
"965178 The Flashbulb - DIDJ PVC.osz",
"966087 The Flashbulb - Creep.osz",
"966277 The Flashbulb - Amen Iraq.osz",
"966407 LukHash - ROOM 12.osz",
"966451 The Flashbulb - Six Acid Strings.osz",
"972301 BilliumMoto - four veiled stars.osz",
"973173 nanobii - popsicle beach.osz",
"973954 BilliumMoto - Rocky Buinne (Short Ver.).osz",
"975435 BilliumMoto - life flashes before weeb eyes.osz",
"978759 L. V. Beethoven - Moonlight Sonata (Cranky Remix).osz",
"982559 BilliumMoto - HDHR.osz",
"984361 The Flashbulb - Ninedump.osz",
"1023681 Inferi - The Ruin of Mankind.osz",
"1034358 ALEPH - The Evil Spirit.osz",
"1037567 ALEPH - Scintillations.osz",
};
private static readonly string[] bundled_catch =
{
"554256 Helblinde - When Time Sleeps.osz",
"693123 yuki. - Nadeshiko Sensation.osz",
"767009 OISHII - PIZZA PLAZA.osz",
"767346 Thaehan - Bwa !.osz",
"815162 VINXIS - Greetings.osz",
"840964 cYsmix - Breeze.osz",
"932657 Wisp X - Eventide.osz",
"933700 onumi - CONFUSION PART ONE.osz",
"933984 onumi - PERSONALITY.osz",
"934785 onumi - FAKE.osz",
"936545 onumi - REGRET PART ONE.osz",
"943803 Fractal Dreamers - Everything for a Dream.osz",
"943876 S3RL - I Will Pick You Up (feat. Tamika).osz",
"946773 Trial & Error - DREAMING COLOR (Short Ver.).osz",
"955808 Trial & Error - Tokoyami no keiyaku KEGARETA-SHOUJO feat. GUMI (Short Ver.).osz",
"957808 Fractal Dreamers - Module_410.osz",
"957842 antiPLUR - One Life Left to Live.osz",
"965730 The Flashbulb - Lawn Wake IV (Black).osz",
"966240 Creo - Challenger.osz",
"968232 Rin - Lunatic set 15 ~ The Moon as Seen from the Shrine.osz",
"972302 VINXIS - A Centralized View.osz",
"972887 HyuN - Illusion of Inflict.osz",
"1008600 LukHash - WHEN AN ANGEL DIES.osz",
"1032103 LukHash - H8 U.osz",
};
private static readonly string[] bundled_mania =
{
"943516 antiPLUR - Clockwork Spooks.osz",
"946394 VINXIS - Three Times The Original Charm.osz",
"966408 antiPLUR - One Life Left to Live.osz",
"971561 antiPLUR - Runengon.osz",
"983864 James Landino - Shiba Island.osz",
"989512 BilliumMoto - 1xMISS.osz",
"994104 James Landino - Reaction feat. Slyleaf.osz",
"1003217 nekodex - circles!.osz",
"1009907 James Landino & Kabuki - Birdsong.osz",
"1015169 Thaehan - Insert Coin.osz",
};
}
}

View File

@ -3,6 +3,7 @@
#nullable enable
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -57,6 +58,26 @@ namespace osu.Game.Graphics.Containers
{
}
/// <summary>
/// Scrolls a <see cref="Drawable"/> into view.
/// </summary>
/// <param name="d">The <see cref="Drawable"/> to scroll into view.</param>
/// <param name="animated">Whether to animate the movement.</param>
/// <param name="extraScroll">An added amount to scroll beyond the requirement to bring the target into view.</param>
public void ScrollIntoView(Drawable d, bool animated = true, float extraScroll = 0)
{
float childPos0 = GetChildPosInContent(d);
float childPos1 = GetChildPosInContent(d, d.DrawSize);
float minPos = Math.Min(childPos0, childPos1);
float maxPos = Math.Max(childPos0, childPos1);
if (minPos < Current || (minPos > Current && d.DrawSize[ScrollDim] > DisplayableContent))
ScrollTo(minPos - extraScroll, animated);
else if (maxPos > Current + DisplayableContent)
ScrollTo(maxPos - DisplayableContent + extraScroll, animated);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (shouldPerformRightMouseScroll(e))

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterfaceV2
{
@ -28,7 +29,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
[BackgroundDependencyLoader(true)]
private void load([CanBeNull] OverlayColourProvider overlayColourProvider, OsuColour colours)
{
BackgroundColour = overlayColourProvider?.Highlight1 ?? colours.Blue3;
if (BackgroundColour == Color4.White)
BackgroundColour = overlayColourProvider?.Highlight1 ?? colours.Blue3;
}
protected override void LoadComplete()

View File

@ -0,0 +1,54 @@
// 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.Localisation;
namespace osu.Game.Localisation
{
public static class FirstRunSetupBeatmapScreenStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.FirstRunSetupBeatmapScreen";
/// <summary>
/// "Obtaining Beatmaps"
/// </summary>
public static LocalisableString Header => new TranslatableString(getKey(@"header"), @"Obtaining Beatmaps");
/// <summary>
/// "&quot;Beatmaps&quot; are what we call playable levels. osu! doesn&#39;t come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection."
/// </summary>
public static LocalisableString Description => new TranslatableString(getKey(@"description"), @"""Beatmaps"" are what we call playable levels. osu! doesn't come with any beatmaps pre-loaded. This step will help you get started on your beatmap collection.");
/// <summary>
/// "If you are a new player, we recommend playing through the tutorial to get accustomed to the gameplay."
/// </summary>
public static LocalisableString TutorialDescription => new TranslatableString(getKey(@"tutorial_description"), @"If you are a new player, we recommend playing through the tutorial to get accustomed to the gameplay.");
/// <summary>
/// "Get the osu! tutorial"
/// </summary>
public static LocalisableString TutorialButton => new TranslatableString(getKey(@"tutorial_button"), @"Get the osu! tutorial");
/// <summary>
/// "To get you started, we have some recommended beatmaps."
/// </summary>
public static LocalisableString BundledDescription => new TranslatableString(getKey(@"bundled_description"), @"To get you started, we have some recommended beatmaps.");
/// <summary>
/// "Get recommended beatmaps"
/// </summary>
public static LocalisableString BundledButton => new TranslatableString(getKey(@"bundled_button"), @"Get recommended beatmaps");
/// <summary>
/// "You can also obtain more beatmaps from the main menu &quot;browse&quot; button at any time."
/// </summary>
public static LocalisableString ObtainMoreBeatmaps => new TranslatableString(getKey(@"obtain_more_beatmaps"), @"You can also obtain more beatmaps from the main menu ""browse"" button at any time.");
/// <summary>
/// "You currently have {0} beatmap(s) loaded!"
/// </summary>
public static LocalisableString CurrentlyLoadedBeatmaps(int beatmaps) => new TranslatableString(getKey(@"currently_loaded_beatmaps"), @"You currently have {0} beatmap(s) loaded!", beatmaps);
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -17,7 +17,8 @@ namespace osu.Game.Localisation
/// <summary>
/// "Click to resume first-run setup at any point"
/// </summary>
public static LocalisableString ClickToResumeFirstRunSetupAtAnyPoint => new TranslatableString(getKey(@"click_to_resume_first_run_setup_at_any_point"), @"Click to resume first-run setup at any point");
public static LocalisableString ClickToResumeFirstRunSetupAtAnyPoint =>
new TranslatableString(getKey(@"click_to_resume_first_run_setup_at_any_point"), @"Click to resume first-run setup at any point");
/// <summary>
/// "First-run setup"
@ -48,6 +49,31 @@ osu! is a very configurable game, and diving straight into the settings can some
/// </summary>
public static LocalisableString UIScaleDescription => new TranslatableString(getKey(@"ui_scale_description"), @"The size of the osu! user interface can be adjusted to your liking.");
/// <summary>
/// "Behaviour"
/// </summary>
public static LocalisableString Behaviour => new TranslatableString(getKey(@"behaviour"), @"Behaviour");
/// <summary>
/// "Some new defaults for game behaviours have been implemented, with the aim of improving the game experience and making it more accessible to everyone.
///
/// We recommend you give the new defaults a try, but if you&#39;d like to have things feel more like classic versions of osu!, you can easily apply some sane defaults below."
/// </summary>
public static LocalisableString BehaviourDescription => new TranslatableString(getKey(@"behaviour_description"),
@"Some new defaults for game behaviours have been implemented, with the aim of improving the game experience and making it more accessible to everyone.
We recommend you give the new defaults a try, but if you'd like to have things feel more like classic versions of osu!, you can easily apply some sane defaults below.");
/// <summary>
/// "New defaults"
/// </summary>
public static LocalisableString NewDefaults => new TranslatableString(getKey(@"new_defaults"), @"New defaults");
/// <summary>
/// "Classic defaults"
/// </summary>
public static LocalisableString ClassicDefaults => new TranslatableString(getKey(@"classic_defaults"), @"Classic defaults");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -0,0 +1,17 @@
// 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 MessagePack;
namespace osu.Game.Online.Multiplayer
{
/// <summary>
/// A <see cref="MultiplayerCountdown"/> started by the server when clients being to load.
/// Indicates how long until gameplay will forcefully start, excluding any users which have not completed loading,
/// and forcing progression of any clients that are blocking load due to user interaction.
/// </summary>
[MessagePackObject]
public class ForceGameplayStartCountdown : MultiplayerCountdown
{
}
}

View File

@ -93,14 +93,20 @@ namespace osu.Game.Online.Multiplayer
Task UserModsChanged(int userId, IEnumerable<APIMod> mods);
/// <summary>
/// Signals that a match is to be started. This will *only* be sent to clients which are to begin loading at this point.
/// Signals that the match is starting and the loading of gameplay should be started. This will *only* be sent to clients which are to begin loading at this point.
/// </summary>
Task LoadRequested();
/// <summary>
/// Signals that a match has started. All users in the <see cref="MultiplayerUserState.Loaded"/> state should begin gameplay as soon as possible.
/// Signals that loading of gameplay is to be aborted.
/// </summary>
Task MatchStarted();
Task LoadAborted();
/// <summary>
/// Signals that gameplay has started.
/// All users in the <see cref="MultiplayerUserState.Loaded"/> or <see cref="MultiplayerUserState.ReadyForGameplay"/> states should begin gameplay as soon as possible.
/// </summary>
Task GameplayStarted();
/// <summary>
/// Signals that the match has ended, all players have finished and results are ready to be displayed.

View File

@ -69,10 +69,15 @@ namespace osu.Game.Online.Multiplayer
/// </summary>
public virtual event Action? LoadRequested;
/// <summary>
/// Invoked when the multiplayer server requests loading of play to be aborted.
/// </summary>
public event Action? LoadAborted;
/// <summary>
/// Invoked when the multiplayer server requests gameplay to be started.
/// </summary>
public event Action? MatchStarted;
public event Action? GameplayStarted;
/// <summary>
/// Invoked when the multiplayer server has finished collating results.
@ -604,14 +609,27 @@ namespace osu.Game.Online.Multiplayer
return Task.CompletedTask;
}
Task IMultiplayerClient.MatchStarted()
Task IMultiplayerClient.LoadAborted()
{
Scheduler.Add(() =>
{
if (Room == null)
return;
MatchStarted?.Invoke();
LoadAborted?.Invoke();
}, false);
return Task.CompletedTask;
}
Task IMultiplayerClient.GameplayStarted()
{
Scheduler.Add(() =>
{
if (Room == null)
return;
GameplayStarted?.Invoke();
}, false);
return Task.CompletedTask;

View File

@ -14,6 +14,7 @@ namespace osu.Game.Online.Multiplayer
/// </summary>
[MessagePackObject]
[Union(0, typeof(MatchStartCountdown))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
[Union(1, typeof(ForceGameplayStartCountdown))]
public abstract class MultiplayerCountdown
{
/// <summary>

View File

@ -65,5 +65,21 @@ namespace osu.Game.Online.Multiplayer
}
public override int GetHashCode() => UserID.GetHashCode();
/// <summary>
/// Whether this user has finished loading and can start gameplay.
/// </summary>
public bool CanStartGameplay()
{
switch (State)
{
case MultiplayerUserState.Loaded:
case MultiplayerUserState.ReadyForGameplay:
return true;
default:
return false;
}
}
}
}

View File

@ -29,10 +29,16 @@ namespace osu.Game.Online.Multiplayer
WaitingForLoad,
/// <summary>
/// The user's client has marked itself as loaded and ready to begin gameplay.
/// The user has marked itself as loaded, but may still be adjusting settings prior to being ready for gameplay.
/// Players remaining in this state for an extended period of time will be automatically transitioned to the <see cref="Playing"/> state by the server.
/// </summary>
Loaded,
/// <summary>
/// The user has finished adjusting settings and is ready to start gameplay.
/// </summary>
ReadyForGameplay,
/// <summary>
/// The user is currently playing in a game. This is a reserved state, and is set by the server.
/// </summary>

View File

@ -54,7 +54,8 @@ namespace osu.Game.Online.Multiplayer
connection.On<MultiplayerRoomSettings>(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged);
connection.On<int, MultiplayerUserState>(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged);
connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested);
connection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted);
connection.On(nameof(IMultiplayerClient.GameplayStarted), ((IMultiplayerClient)this).GameplayStarted);
connection.On(nameof(IMultiplayerClient.LoadAborted), ((IMultiplayerClient)this).LoadAborted);
connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady);
connection.On<int, IEnumerable<APIMod>>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged);
connection.On<int, BeatmapAvailability>(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged);

View File

@ -10,8 +10,8 @@ namespace osu.Game.Online
WebsiteRootUrl = APIEndpointUrl = @"https://osu.ppy.sh";
APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk";
APIClientID = "5";
SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator";
MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer";
SpectatorEndpointUrl = "https://spectator2.ppy.sh/spectator";
MultiplayerEndpointUrl = "https://spectator2.ppy.sh/multiplayer";
}
}
}

View File

@ -24,7 +24,8 @@ namespace osu.Game.Online
(typeof(CountdownChangedEvent), typeof(MatchServerEvent)),
(typeof(TeamVersusRoomState), typeof(MatchRoomState)),
(typeof(TeamVersusUserState), typeof(MatchUserState)),
(typeof(MatchStartCountdown), typeof(MultiplayerCountdown))
(typeof(MatchStartCountdown), typeof(MultiplayerCountdown)),
(typeof(ForceGameplayStartCountdown), typeof(MultiplayerCountdown))
};
}
}

View File

@ -19,8 +19,11 @@ namespace osu.Game.Overlays.FirstRunSetup
protected FillFlowContainer Content { get; private set; }
[Resolved]
protected OverlayColourProvider OverlayColourProvider { get; private set; }
[BackgroundDependencyLoader]
private void load(OverlayColourProvider overlayColourProvider)
private void load()
{
const float header_size = 40;
const float spacing = 20;
@ -30,13 +33,14 @@ namespace osu.Game.Overlays.FirstRunSetup
new OsuScrollContainer(Direction.Vertical)
{
RelativeSizeAxes = Axes.Both,
ScrollbarOverlapsContent = false,
Children = new Drawable[]
{
new OsuSpriteText
{
Text = this.GetLocalisableDescription(),
Font = OsuFont.Default.With(size: header_size),
Colour = overlayColourProvider.Light1,
Colour = OverlayColourProvider.Light1,
},
Content = new FillFlowContainer
{

View File

@ -0,0 +1,258 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Online;
using osuTK;
using Realms;
namespace osu.Game.Overlays.FirstRunSetup
{
[LocalisableDescription(typeof(FirstRunSetupBeatmapScreenStrings), nameof(FirstRunSetupBeatmapScreenStrings.Header))]
public class ScreenBeatmaps : FirstRunSetupScreen
{
private ProgressRoundedButton downloadBundledButton = null!;
private ProgressRoundedButton importBeatmapsButton = null!;
private ProgressRoundedButton downloadTutorialButton = null!;
private OsuTextFlowContainer currentlyLoadedBeatmaps = null!;
private BundledBeatmapDownloader? tutorialDownloader;
private BundledBeatmapDownloader? bundledDownloader;
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private RealmAccess realmAccess { get; set; } = null!;
private IDisposable? beatmapSubscription;
[BackgroundDependencyLoader(permitNulls: true)]
private void load(LegacyImportManager? legacyImportManager)
{
Vector2 buttonSize = new Vector2(500, 60);
Content.Children = new Drawable[]
{
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20))
{
Colour = OverlayColourProvider.Content1,
Text = FirstRunSetupBeatmapScreenStrings.Description,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
},
new Container
{
RelativeSizeAxes = Axes.X,
Height = 30,
Children = new Drawable[]
{
currentlyLoadedBeatmaps = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 24, weight: FontWeight.SemiBold))
{
Colour = OverlayColourProvider.Content2,
TextAnchor = Anchor.Centre,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
},
}
},
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20))
{
Colour = OverlayColourProvider.Content1,
Text = FirstRunSetupBeatmapScreenStrings.TutorialDescription,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
},
downloadTutorialButton = new ProgressRoundedButton
{
Size = buttonSize,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
BackgroundColour = colours.Pink3,
Text = FirstRunSetupBeatmapScreenStrings.TutorialButton,
Action = downloadTutorial
},
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20))
{
Colour = OverlayColourProvider.Content1,
Text = FirstRunSetupBeatmapScreenStrings.BundledDescription,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
},
downloadBundledButton = new ProgressRoundedButton
{
Size = buttonSize,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
BackgroundColour = colours.Blue3,
Text = FirstRunSetupBeatmapScreenStrings.BundledButton,
Action = downloadBundled
},
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20))
{
Colour = OverlayColourProvider.Content1,
Text = "If you have an existing osu! install, you can also choose to import your existing beatmap collection.",
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
},
importBeatmapsButton = new ProgressRoundedButton
{
Size = buttonSize,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
BackgroundColour = colours.Blue3,
Text = MaintenanceSettingsStrings.ImportBeatmapsFromStable,
Action = () =>
{
importBeatmapsButton.Enabled.Value = false;
legacyImportManager?.ImportFromStableAsync(StableContent.Beatmaps).ContinueWith(t => Schedule(() =>
{
if (t.IsCompletedSuccessfully)
importBeatmapsButton.Complete();
else
importBeatmapsButton.Enabled.Value = true;
}));
}
},
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20))
{
Colour = OverlayColourProvider.Content1,
Text = FirstRunSetupBeatmapScreenStrings.ObtainMoreBeatmaps,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
beatmapSubscription = realmAccess.RegisterForNotifications(r => r.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected), beatmapsChanged);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
beatmapSubscription?.Dispose();
}
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error)
{
currentlyLoadedBeatmaps.Text = FirstRunSetupBeatmapScreenStrings.CurrentlyLoadedBeatmaps(sender.Count);
if (sender.Count == 0)
{
currentlyLoadedBeatmaps.FadeColour(colours.Red1, 500, Easing.OutQuint);
}
else if (changes != null && (changes.DeletedIndices.Any() || changes.InsertedIndices.Any()))
{
currentlyLoadedBeatmaps.FadeColour(colours.Yellow)
.FadeColour(OverlayColourProvider.Content2, 1500, Easing.OutQuint);
currentlyLoadedBeatmaps.ScaleTo(1.1f)
.ScaleTo(1, 1500, Easing.OutQuint);
}
}
private void downloadTutorial()
{
if (tutorialDownloader != null)
return;
tutorialDownloader = new BundledBeatmapDownloader(true);
AddInternal(tutorialDownloader);
var downloadTracker = tutorialDownloader.DownloadTrackers.First();
downloadTracker.Progress.BindValueChanged(progress =>
{
downloadTutorialButton.SetProgress(progress.NewValue, false);
if (progress.NewValue == 1)
downloadTutorialButton.Complete();
}, true);
}
private void downloadBundled()
{
if (bundledDownloader != null)
return;
bundledDownloader = new BundledBeatmapDownloader(false);
AddInternal(bundledDownloader);
foreach (var tracker in bundledDownloader.DownloadTrackers)
tracker.State.BindValueChanged(_ => updateProgress(), true);
void updateProgress()
{
double progress = (double)bundledDownloader.DownloadTrackers.Count(t => t.State.Value == DownloadState.LocallyAvailable) / bundledDownloader.DownloadTrackers.Count();
if (progress == 1)
downloadBundledButton.Complete();
else
downloadBundledButton.SetProgress(progress, true);
}
}
private class ProgressRoundedButton : RoundedButton
{
[Resolved]
private OsuColour colours { get; set; } = null!;
private ProgressBar progressBar = null!;
protected override void LoadComplete()
{
base.LoadComplete();
Add(progressBar = new ProgressBar(false)
{
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
FillColour = BackgroundColour,
Alpha = 0.5f,
Depth = float.MinValue
});
}
public void Complete()
{
Enabled.Value = false;
Background.FadeColour(colours.Green, 500, Easing.OutQuint);
progressBar.FillColour = colours.Green;
this.TransformBindableTo(progressBar.Current, 1, 500, Easing.OutQuint);
}
public void SetProgress(double progress, bool animated)
{
if (!Enabled.Value)
return;
this.TransformBindableTo(progressBar.Current, progress, animated ? 500 : 0, Easing.OutQuint);
}
}
}
}

View File

@ -0,0 +1,109 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections;
namespace osu.Game.Overlays.FirstRunSetup
{
[LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.Behaviour))]
public class ScreenBehaviour : FirstRunSetupScreen
{
private SearchContainer<SettingsSection> searchContainer;
[BackgroundDependencyLoader]
private void load()
{
Content.Children = new Drawable[]
{
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 24))
{
Text = FirstRunSetupOverlayStrings.BehaviourDescription,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
},
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.Absolute, 10),
new Dimension(),
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new[]
{
new TriangleButton
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Text = FirstRunSetupOverlayStrings.NewDefaults,
RelativeSizeAxes = Axes.X,
Action = applyStandard,
},
Empty(),
new DangerousTriangleButton
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Text = FirstRunSetupOverlayStrings.ClassicDefaults,
RelativeSizeAxes = Axes.X,
Action = applyClassic
}
},
},
},
searchContainer = new SearchContainer<SettingsSection>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new SettingsSection[]
{
// This list should be kept in sync with SettingsOverlay.
new GeneralSection(),
new SkinSection(),
// InputSection is intentionally omitted for now due to its sub-panel being a pain to set up.
new UserInterfaceSection(),
new GameplaySection(),
new RulesetSection(),
new AudioSection(),
new GraphicsSection(),
new OnlineSection(),
new MaintenanceSection(),
new DebugSection(),
},
SearchTerm = SettingsItem<bool>.CLASSIC_DEFAULT_SEARCH_TERM,
}
};
}
private void applyClassic()
{
foreach (var i in searchContainer.ChildrenOfType<ISettingsItem>().Where(s => s.HasClassicDefault))
i.ApplyClassicDefault();
}
private void applyStandard()
{
foreach (var i in searchContainer.ChildrenOfType<ISettingsItem>().Where(s => s.HasClassicDefault))
i.ApplyDefault();
}
}
}

View File

@ -25,7 +25,6 @@ using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Match.Components;
namespace osu.Game.Overlays
{
@ -45,8 +44,8 @@ namespace osu.Game.Overlays
private ScreenStack? stack;
public PurpleTriangleButton NextButton = null!;
public DangerousTriangleButton BackButton = null!;
public ShearedButton NextButton = null!;
public ShearedButton BackButton = null!;
private readonly Bindable<bool> showFirstRunSetup = new Bindable<bool>();
@ -60,7 +59,9 @@ namespace osu.Game.Overlays
private readonly Type[] steps =
{
typeof(ScreenWelcome),
typeof(ScreenUIScale)
typeof(ScreenBeatmaps),
typeof(ScreenUIScale),
typeof(ScreenBehaviour),
};
private Container stackContainer = null!;
@ -70,7 +71,7 @@ namespace osu.Game.Overlays
private Container content = null!;
[BackgroundDependencyLoader]
private void load()
private void load(OsuColour colours)
{
Header.Title = FirstRunSetupOverlayStrings.FirstRunSetupTitle;
Header.Description = FirstRunSetupOverlayStrings.FirstRunSetupDescription;
@ -82,7 +83,11 @@ namespace osu.Game.Overlays
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = 50 },
Padding = new MarginPadding
{
Horizontal = 70 * 1.2f,
Bottom = 20,
},
Child = new InputBlockingContainer
{
Masking = true,
@ -103,7 +108,7 @@ namespace osu.Game.Overlays
Padding = new MarginPadding
{
Vertical = 20,
Horizontal = 20,
Horizontal = 70,
},
}
},
@ -115,14 +120,15 @@ namespace osu.Game.Overlays
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Width = 0.98f,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Vertical = PADDING },
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 10),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 10),
},
RowDimensions = new[]
{
@ -132,21 +138,25 @@ namespace osu.Game.Overlays
{
new[]
{
BackButton = new DangerousTriangleButton
Empty(),
BackButton = new ShearedButton(300)
{
Width = 300,
Text = CommonStrings.Back,
Action = showPreviousStep,
Enabled = { Value = false },
DarkerColour = colours.Pink2,
LighterColour = colours.Pink1,
},
Empty(),
NextButton = new PurpleTriangleButton
NextButton = new ShearedButton(0)
{
RelativeSizeAxes = Axes.X,
Width = 1,
Text = FirstRunSetupOverlayStrings.GetStarted,
DarkerColour = ColourProvider.Colour2,
LighterColour = ColourProvider.Colour1,
Action = showNextStep
}
},
Empty(),
},
}
});

View File

@ -53,6 +53,9 @@ namespace osu.Game.Overlays.Mods
}
public Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
public Bindable<bool> Active = new BindableBool(true);
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value;
protected virtual ModPanel CreateModPanel(Mod mod) => new ModPanel(mod);

View File

@ -13,7 +13,9 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Mods;
@ -59,7 +61,8 @@ namespace osu.Game.Overlays.Mods
private DifficultyMultiplierDisplay? multiplierDisplay;
private ModSettingsArea modSettingsArea = null!;
private FillFlowContainer<ModColumn> columnFlow = null!;
private ColumnScrollContainer columnScroll = null!;
private ColumnFlowContainer columnFlow = null!;
[BackgroundDependencyLoader]
private void load()
@ -95,27 +98,27 @@ namespace osu.Game.Overlays.Mods
RelativePositionAxes = Axes.Both,
Children = new Drawable[]
{
new OsuScrollContainer(Direction.Horizontal)
columnScroll = new ColumnScrollContainer
{
RelativeSizeAxes = Axes.Both,
Masking = false,
ClampExtension = 100,
ScrollbarOverlapsContent = false,
Child = columnFlow = new ModColumnContainer
Child = columnFlow = new ColumnFlowContainer
{
Direction = FillDirection.Horizontal,
Shear = new Vector2(SHEAR, 0),
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Spacing = new Vector2(10, 0),
Margin = new MarginPadding { Right = 70 },
Margin = new MarginPadding { Horizontal = 70 },
Children = new[]
{
CreateModColumn(ModType.DifficultyReduction, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }),
CreateModColumn(ModType.DifficultyIncrease, new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L }),
CreateModColumn(ModType.Automation, new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }),
CreateModColumn(ModType.Conversion),
CreateModColumn(ModType.Fun)
createModColumnContent(ModType.DifficultyReduction, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }),
createModColumnContent(ModType.DifficultyIncrease, new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L }),
createModColumnContent(ModType.Automation, new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }),
createModColumnContent(ModType.Conversion),
createModColumnContent(ModType.Fun)
}
}
}
@ -153,6 +156,14 @@ namespace osu.Game.Overlays.Mods
}
}
private ColumnDimContainer createModColumnContent(ModType modType, Key[]? toggleKeys = null)
=> new ColumnDimContainer(CreateModColumn(modType, toggleKeys))
{
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
RequestScroll = column => columnScroll.ScrollIntoView(column, extraScroll: 140)
};
protected override void LoadComplete()
{
base.LoadComplete();
@ -166,7 +177,7 @@ namespace osu.Game.Overlays.Mods
updateSelectionFromBindable();
}, true);
foreach (var column in columnFlow)
foreach (var column in columnFlow.Columns)
{
column.SelectedMods.BindValueChanged(updateBindableFromSelection);
}
@ -191,7 +202,7 @@ namespace osu.Game.Overlays.Mods
private void updateAvailableMods()
{
foreach (var column in columnFlow)
foreach (var column in columnFlow.Columns)
column.Filter = isValidMod;
}
@ -244,7 +255,7 @@ namespace osu.Game.Overlays.Mods
// to synchronise state correctly, updateBindableFromSelection() computes the final mods (including incompatibility rules) and updates SelectedMods,
// and this method then runs unconditionally again to make sure the new visual selection accurately reflects the final set of selected mods.
// selectionBindableSyncInProgress ensures that mutual infinite recursion does not happen after that unconditional call.
foreach (var column in columnFlow)
foreach (var column in columnFlow.Columns)
column.SelectedMods.Value = SelectedMods.Value.Where(mod => mod.Type == column.ModType).ToArray();
}
@ -265,7 +276,7 @@ namespace osu.Game.Overlays.Mods
}
protected virtual IReadOnlyList<Mod> ComputeNewModsFromSelection(IEnumerable<Mod> addedMods, IEnumerable<Mod> removedMods)
=> columnFlow.SelectMany(column => column.SelectedMods.Value).ToArray();
=> columnFlow.Columns.SelectMany(column => column.SelectedMods.Value).ToArray();
protected override void PopIn()
{
@ -280,7 +291,8 @@ namespace osu.Game.Overlays.Mods
for (int i = 0; i < columnFlow.Count; i++)
{
columnFlow[i].TopLevelContent
columnFlow[i].Column
.TopLevelContent
.Delay(i * 30)
.MoveToY(0, fade_in_duration, Easing.OutQuint)
.FadeIn(fade_in_duration, Easing.OutQuint);
@ -301,27 +313,68 @@ namespace osu.Game.Overlays.Mods
{
const float distance = 700;
columnFlow[i].TopLevelContent
columnFlow[i].Column
.TopLevelContent
.MoveToY(i % 2 == 0 ? -distance : distance, fade_out_duration, Easing.OutQuint)
.FadeOut(fade_out_duration, Easing.OutQuint);
}
}
private class ModColumnContainer : FillFlowContainer<ModColumn>
internal class ColumnScrollContainer : OsuScrollContainer<ColumnFlowContainer>
{
public ColumnScrollContainer()
: base(Direction.Horizontal)
{
}
protected override void Update()
{
base.Update();
// the bounds below represent the horizontal range of scroll items to be considered fully visible/active, in the scroll's internal coordinate space.
// note that clamping is applied to the left scroll bound to ensure scrolling past extents does not change the set of active columns.
float leftVisibleBound = Math.Clamp(Current, 0, ScrollableExtent);
float rightVisibleBound = leftVisibleBound + DrawWidth;
// if a movement is occurring at this time, the bounds below represent the full range of columns that the scroll movement will encompass.
// this will be used to ensure that columns do not change state from active to inactive back and forth until they are fully scrolled past.
float leftMovementBound = Math.Min(Current, Target);
float rightMovementBound = Math.Max(Current, Target) + DrawWidth;
foreach (var column in Child)
{
// DrawWidth/DrawPosition do not include shear effects, and we want to know the full extents of the columns post-shear,
// so we have to manually compensate.
var topLeft = column.ToSpaceOfOtherDrawable(Vector2.Zero, ScrollContent);
var bottomRight = column.ToSpaceOfOtherDrawable(new Vector2(column.DrawWidth - column.DrawHeight * SHEAR, 0), ScrollContent);
bool isCurrentlyVisible = Precision.AlmostBigger(topLeft.X, leftVisibleBound)
&& Precision.DefinitelyBigger(rightVisibleBound, bottomRight.X);
bool isBeingScrolledToward = Precision.AlmostBigger(topLeft.X, leftMovementBound)
&& Precision.DefinitelyBigger(rightMovementBound, bottomRight.X);
column.Active.Value = isCurrentlyVisible || isBeingScrolledToward;
}
}
}
internal class ColumnFlowContainer : FillFlowContainer<ColumnDimContainer>
{
public IEnumerable<ModColumn> Columns => Children.Select(dimWrapper => dimWrapper.Column);
private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize);
public ModColumnContainer()
public ColumnFlowContainer()
{
AddLayout(drawSizeLayout);
}
public override void Add(ModColumn column)
public override void Add(ColumnDimContainer dimContainer)
{
base.Add(column);
base.Add(dimContainer);
Debug.Assert(column != null);
column.Shear = Vector2.Zero;
Debug.Assert(dimContainer != null);
dimContainer.Column.Shear = Vector2.Zero;
}
protected override void Update()
@ -341,6 +394,63 @@ namespace osu.Game.Overlays.Mods
}
}
internal class ColumnDimContainer : Container
{
public ModColumn Column { get; }
public readonly Bindable<bool> Active = new BindableBool();
public Action<ColumnDimContainer>? RequestScroll { get; set; }
[Resolved]
private OsuColour colours { get; set; } = null!;
public ColumnDimContainer(ModColumn column)
{
Child = Column = column;
column.Active.BindTo(Active);
}
protected override void LoadComplete()
{
base.LoadComplete();
Active.BindValueChanged(_ => updateDim(), true);
FinishTransforms();
}
private void updateDim()
{
Colour4 targetColour;
if (Active.Value)
targetColour = Colour4.White;
else
targetColour = IsHovered ? colours.GrayC : colours.Gray8;
this.FadeColour(targetColour, 800, Easing.OutQuint);
}
protected override bool OnClick(ClickEvent e)
{
if (!Active.Value)
RequestScroll?.Invoke(this);
return true;
}
protected override bool OnHover(HoverEvent e)
{
base.OnHover(e);
updateDim();
return Active.Value;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
updateDim();
}
}
private class ClickToReturnContainer : Container
{
public BindableBool HandleMouse { get; } = new BindableBool();

View File

@ -14,13 +14,13 @@ namespace osu.Game.Overlays.Mods
/// </summary>
public class NestedVerticalScrollContainer : OsuScrollContainer
{
private OsuScrollContainer? parentScrollContainer;
private ModSelectScreen.ColumnScrollContainer? parentScrollContainer;
protected override void LoadComplete()
{
base.LoadComplete();
parentScrollContainer = this.FindClosestParent<OsuScrollContainer>();
parentScrollContainer = this.FindClosestParent<ModSelectScreen.ColumnScrollContainer>();
}
protected override bool OnScroll(ScrollEvent e)

View File

@ -31,7 +31,9 @@ namespace osu.Game.Overlays.Profile
User.ValueChanged += e => updateDisplay(e.NewValue);
TabControl.AddItem(LayoutStrings.HeaderUsersShow);
TabControl.AddItem(LayoutStrings.HeaderUsersModding);
// todo: pending implementation.
// TabControl.AddItem(LayoutStrings.HeaderUsersModding);
centreHeaderContainer.DetailsVisible.BindValueChanged(visible => detailHeaderContainer.Expanded = visible.NewValue, true);
}

View File

@ -9,5 +9,20 @@ namespace osu.Game.Overlays.Settings
public interface ISettingsItem : IDrawable, IDisposable
{
event Action SettingChanged;
/// <summary>
/// Whether this setting has a classic default (ie. a different default which better aligns with osu-stable expectations).
/// </summary>
bool HasClassicDefault { get; }
/// <summary>
/// Apply the classic default value of the associated setting. Will throw if <see cref="HasClassicDefault"/> is <c>false</c>.
/// </summary>
void ApplyClassicDefault();
/// <summary>
/// Apply the default value of the associated setting.
/// </summary>
void ApplyDefault();
}
}

View File

@ -28,6 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
},
new SettingsCheckbox
{
ClassicDefault = false,
LabelText = GameplaySettingsStrings.AlwaysPlayFirstComboBreak,
Current = config.GetBindable<bool>(OsuSetting.AlwaysPlayFirstComboBreak)
}

View File

@ -21,6 +21,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
{
new SettingsEnumDropdown<ScoringMode>
{
ClassicDefault = ScoringMode.Classic,
LabelText = GameplaySettingsStrings.ScoreDisplayMode,
Current = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode),
Keywords = new[] { "scoring" }

View File

@ -30,6 +30,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
},
new SettingsCheckbox
{
ClassicDefault = false,
LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail,
Current = config.GetBindable<bool>(OsuSetting.ShowHealthDisplayWhenCantFail),
Keywords = new[] { "hp", "bar" }

View File

@ -37,6 +37,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
},
new SettingsSlider<double, TimeSlider>
{
ClassicDefault = 0,
LabelText = UserInterfaceStrings.HoldToConfirmActivationTime,
Current = config.GetBindable<double>(OsuSetting.UIHoldActivationDelay),
Keywords = new[] { @"delay" },

View File

@ -32,6 +32,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
{
new SettingsCheckbox
{
ClassicDefault = true,
LabelText = UserInterfaceStrings.RightMouseScroll,
Current = config.GetBindable<bool>(OsuSetting.SongSelectRightMouseScroll),
},

View File

@ -30,6 +30,8 @@ namespace osu.Game.Overlays.Settings
/// </summary>
public object SettingSourceObject { get; internal set; }
public const string CLASSIC_DEFAULT_SEARCH_TERM = @"has-classic-default";
private IHasCurrentValue<T> controlWithCurrent => Control as IHasCurrentValue<T>;
protected override Container<Drawable> Content => FlowContent;
@ -96,7 +98,21 @@ namespace osu.Game.Overlays.Settings
set => controlWithCurrent.Current = value;
}
public virtual IEnumerable<string> FilterTerms => Keywords == null ? new[] { LabelText.ToString() } : new List<string>(Keywords) { LabelText.ToString() }.ToArray();
public virtual IEnumerable<string> FilterTerms
{
get
{
var keywords = new List<string>(Keywords ?? Array.Empty<string>())
{
LabelText.ToString()
};
if (HasClassicDefault)
keywords.Add(CLASSIC_DEFAULT_SEARCH_TERM);
return keywords;
}
}
public IEnumerable<string> Keywords { get; set; }
@ -122,6 +138,32 @@ namespace osu.Game.Overlays.Settings
public event Action SettingChanged;
private T classicDefault;
public bool HasClassicDefault { get; private set; }
/// <summary>
/// A "classic" default value for this setting.
/// </summary>
public T ClassicDefault
{
set
{
classicDefault = value;
HasClassicDefault = true;
}
}
public void ApplyClassicDefault()
{
if (!HasClassicDefault)
throw new InvalidOperationException($"Cannot apply a classic default to a setting which doesn't have one defined via {nameof(ClassicDefault)}.");
Current.Value = classicDefault;
}
public void ApplyDefault() => Current.SetDefault();
protected SettingsItem()
{
RelativeSizeAxes = Axes.X;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -58,7 +59,7 @@ namespace osu.Game.Overlays.Settings
public bool FilteringActive { get; set; }
[Resolved]
[Resolved(canBeNull: true)]
private SettingsPanel settingsPanel { get; set; }
protected SettingsSection()
@ -131,7 +132,7 @@ namespace osu.Game.Overlays.Settings
},
});
selectedSection = settingsPanel.CurrentSection.GetBoundCopy();
selectedSection = settingsPanel?.CurrentSection.GetBoundCopy() ?? new Bindable<SettingsSection>(this);
selectedSection.BindValueChanged(_ => updateContentFade(), true);
}
@ -152,7 +153,10 @@ namespace osu.Game.Overlays.Settings
protected override bool OnClick(ClickEvent e)
{
if (!isCurrentSection)
{
Debug.Assert(settingsPanel != null);
settingsPanel.SectionsContainer.ScrollTo(this);
}
return base.OnClick(e);
}

View File

@ -23,6 +23,7 @@ namespace osu.Game.Overlays
protected override IEnumerable<SettingsSection> CreateSections() => new SettingsSection[]
{
// This list should be kept in sync with ScreenBehaviour.
new GeneralSection(),
new SkinSection(),
new InputSection(createSubPanel(new KeyBindingPanel())),

View File

@ -405,6 +405,8 @@ namespace osu.Game.Screens.Menu
private class DragContainer : Container
{
public override bool DragBlocksClick => false;
protected override bool OnDragStart(DragStartEvent e) => true;
protected override void OnDrag(DragEvent e)

View File

@ -77,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void onRoomUpdated() => Scheduler.AddOnce(() =>
{
bool countdownActive = multiplayerClient.Room?.Countdown != null;
bool countdownActive = multiplayerClient.Room?.Countdown is MatchStartCountdown;
if (countdownActive)
{

View File

@ -55,7 +55,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void onRoomUpdated() => Scheduler.AddOnce(() =>
{
if (countdown != room?.Countdown)
MultiplayerCountdown newCountdown;
switch (room?.Countdown)
{
case MatchStartCountdown _:
newCountdown = room.Countdown;
break;
// Clear the countdown with any other (including non-null) countdown values.
default:
newCountdown = null;
break;
}
if (newCountdown != countdown)
{
countdown = room?.Countdown;
countdownChangeTime = Time.Current;

View File

@ -3,6 +3,7 @@
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.OnlinePlay.Components;
@ -20,6 +21,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
base.LoadComplete();
client.RoomUpdated += onRoomUpdated;
client.LoadAborted += onLoadAborted;
onRoomUpdated();
}
@ -35,6 +37,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
transitionFromResults();
}
private void onLoadAborted()
{
// If the server aborts gameplay for this user (due to loading too slow), exit gameplay screens.
if (!this.IsCurrentScreen())
{
Logger.Log("Gameplay aborted because loading the beatmap took too long.", LoggingTarget.Runtime, LogLevel.Important);
this.MakeCurrent();
}
}
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(e);
@ -42,9 +54,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client.Room == null)
return;
Debug.Assert(client.LocalUser != null);
if (!(e.Last is MultiplayerPlayerLoader playerLoader))
return;
// Nothing needs to be done if already in the idle state (e.g. via load being aborted by the server).
if (client.LocalUser.State == MultiplayerUserState.Idle)
return;
// If gameplay wasn't finished, then we have a simple path back to the idle state by aborting gameplay.
if (!playerLoader.GameplayPassed)
{

View File

@ -115,7 +115,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (!ValidForResume)
return; // token retrieval may have failed.
client.MatchStarted += onMatchStarted;
client.GameplayStarted += onGameplayStarted;
client.ResultsReady += onResultsReady;
ScoreProcessor.HasCompleted.BindValueChanged(completed =>
@ -144,10 +144,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override void StartGameplay()
{
// block base call, but let the server know we are ready to start.
loadingDisplay.Show();
client.ChangeState(MultiplayerUserState.Loaded).ContinueWith(task => failAndBail(task.Exception?.Message ?? "Server error"), TaskContinuationOptions.NotOnRanToCompletion);
if (client.LocalUser?.State == MultiplayerUserState.Loaded)
{
// block base call, but let the server know we are ready to start.
loadingDisplay.Show();
client.ChangeState(MultiplayerUserState.ReadyForGameplay);
}
}
private void failAndBail(string message = null)
@ -175,7 +177,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
leaderboardFlow.Position = new Vector2(padding, padding + HUDOverlay.TopScoringElementsHeight);
}
private void onMatchStarted() => Scheduler.Add(() =>
private void onGameplayStarted() => Scheduler.Add(() =>
{
if (!this.IsCurrentScreen())
return;
@ -223,7 +225,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client != null)
{
client.MatchStarted -= onMatchStarted;
client.GameplayStarted -= onGameplayStarted;
client.ResultsReady -= onResultsReady;
}
}

View File

@ -2,7 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
@ -11,6 +15,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public bool GameplayPassed => player?.GameplayState.HasPassed == true;
[Resolved]
private MultiplayerClient multiplayerClient { get; set; }
private Player player;
public MultiplayerPlayerLoader(Func<Player> createPlayer)
@ -18,6 +25,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
}
protected override bool ReadyForGameplay =>
base.ReadyForGameplay
// The server is forcefully starting gameplay.
|| multiplayerClient.LocalUser?.State == MultiplayerUserState.Playing;
protected override void OnPlayerLoaded()
{
base.OnPlayerLoaded();
multiplayerClient.ChangeState(MultiplayerUserState.Loaded)
.ContinueWith(task => failAndBail(task.Exception?.Message ?? "Server error"), TaskContinuationOptions.NotOnRanToCompletion);
}
private void failAndBail(string message = null)
{
if (!string.IsNullOrEmpty(message))
Logger.Log(message, LoggingTarget.Runtime, LogLevel.Important);
Schedule(() =>
{
if (this.IsCurrentScreen())
this.Exit();
});
}
public override void OnSuspending(ScreenTransitionEvent e)
{
base.OnSuspending(e);

View File

@ -112,6 +112,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
break;
case MultiplayerUserState.Loaded:
case MultiplayerUserState.ReadyForGameplay:
text.Text = "loaded";
icon.Icon = FontAwesome.Solid.DotCircle;
icon.Colour = colours.YellowLight;

View File

@ -92,11 +92,15 @@ namespace osu.Game.Screens.Play
!playerConsumed
// don't push unless the player is completely loaded
&& CurrentPlayer?.LoadState == LoadState.Ready
// don't push if the user is hovering one of the panes, unless they are idle.
&& (IsHovered || idleTracker.IsIdle.Value)
// don't push if the user is dragging a slider or otherwise.
// don't push unless the player is ready to start gameplay
&& ReadyForGameplay;
protected virtual bool ReadyForGameplay =>
// not ready if the user is hovering one of the panes, unless they are idle.
(IsHovered || idleTracker.IsIdle.Value)
// not ready if the user is dragging a slider or otherwise.
&& inputManager.DraggedDrawable == null
// don't push if a focused overlay is visible, like settings.
// not ready if a focused overlay is visible, like settings.
&& inputManager.FocusedDrawable == null;
private readonly Func<Player> createPlayer;
@ -364,7 +368,15 @@ namespace osu.Game.Screens.Play
CurrentPlayer.RestartCount = restartCount++;
CurrentPlayer.RestartRequested = restartRequested;
LoadTask = LoadComponentAsync(CurrentPlayer, _ => MetadataInfo.Loading = false);
LoadTask = LoadComponentAsync(CurrentPlayer, _ =>
{
MetadataInfo.Loading = false;
OnPlayerLoaded();
});
}
protected virtual void OnPlayerLoaded()
{
}
private void restartRequested()

View File

@ -27,8 +27,9 @@ namespace osu.Game.Screens.Select.Filter
[LocalisableDescription(typeof(SortStrings), nameof(SortStrings.ArtistTracksLength))]
Length,
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchFiltersRank))]
RankAchieved,
// todo: pending support (https://github.com/ppy/osu/issues/4917)
// [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.ListingSearchFiltersRank))]
// RankAchieved,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoSource))]
Source,

View File

@ -10,6 +10,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Input.Bindings;
using osuTK;
using osuTK.Input;
namespace osu.Game.Screens.Select
{
@ -25,7 +26,7 @@ namespace osu.Game.Screens.Select
{
SelectedColour = colours.Green;
DeselectedColour = SelectedColour.Opacity(0.5f);
Text = @"random";
updateText();
Action = () =>
{
@ -59,6 +60,44 @@ namespace osu.Game.Screens.Select
};
}
protected override bool OnKeyDown(KeyDownEvent e)
{
updateText(e.ShiftPressed);
return base.OnKeyDown(e);
}
protected override void OnKeyUp(KeyUpEvent e)
{
updateText(e.ShiftPressed);
base.OnKeyUp(e);
}
protected override bool OnClick(ClickEvent e)
{
try
{
// this uses OR to handle rewinding when clicks are triggered by other sources (i.e. right button in OnMouseUp).
rewindSearch |= e.ShiftPressed;
return base.OnClick(e);
}
finally
{
rewindSearch = false;
}
}
protected override void OnMouseUp(MouseUpEvent e)
{
if (e.Button == MouseButton.Right)
{
rewindSearch = true;
TriggerClick();
return;
}
base.OnMouseUp(e);
}
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
rewindSearch = e.Action == GlobalAction.SelectPreviousRandom;
@ -79,5 +118,7 @@ namespace osu.Game.Screens.Select
rewindSearch = false;
}
}
private void updateText(bool rewind = false) => Text = rewind ? "rewind" : "random";
}
}

View File

@ -155,7 +155,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded))
ChangeUserState(u.UserID, MultiplayerUserState.Playing);
((IMultiplayerClient)this).MatchStarted();
((IMultiplayerClient)this).GameplayStarted();
ChangeRoomState(MultiplayerRoomState.Playing);
}

View File

@ -35,7 +35,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="10.10.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.428.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.430.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.422.0" />
<PackageReference Include="Sentry" Version="3.14.1" />
<PackageReference Include="SharpCompress" Version="0.30.1" />

View File

@ -61,7 +61,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.428.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.430.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.422.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
@ -84,7 +84,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2022.428.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.430.0" />
<PackageReference Include="SharpCompress" Version="0.30.1" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />