1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-08 10:23:18 +08:00

Merge branch 'master' into multiplayer-free-style

This commit is contained in:
Dean Herbert 2025-01-24 17:23:35 +09:00
commit 1bfb4d4c2b
No known key found for this signature in database
43 changed files with 684 additions and 189 deletions

View File

@ -96,7 +96,7 @@ jobs:
build-only-android:
name: Build only (Android)
runs-on: windows-latest
runs-on: windows-2019
timeout-minutes: 60
steps:
- name: Checkout

View File

@ -82,7 +82,7 @@ namespace osu.Desktop
};
client.OnReady += onReady;
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error);
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network);
try
{

View File

@ -30,8 +30,6 @@ namespace osu.Desktop.Security
private partial class ElevatedPrivilegesNotification : SimpleNotification
{
public override bool IsImportant => true;
public ElevatedPrivilegesNotification()
{
Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.";

View File

@ -4,6 +4,7 @@
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@ -47,6 +48,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return new DefaultSkinComponentsContainer(container =>
{
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
if (keyCounter != null)
{
@ -55,11 +57,19 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
keyCounter.Origin = Anchor.TopRight;
keyCounter.Position = new Vector2(0, -40) * 1.6f;
}
if (spectatorList != null)
{
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = new Vector2(10, -10);
}
})
{
Children = new Drawable[]
{
new LegacyKeyCounterDisplay(),
new SpectatorList(),
}
};
}

View File

@ -9,7 +9,9 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.Argon
@ -39,6 +41,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
return new DefaultSkinComponentsContainer(container =>
{
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
if (combo != null)
{
@ -47,9 +50,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
combo.Origin = Anchor.Centre;
combo.Y = 200;
}
if (spectatorList != null)
spectatorList.Position = new Vector2(36, -66);
})
{
new ArgonManiaComboCounter(),
new SpectatorList
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
}
};
}

View File

@ -15,7 +15,9 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
@ -95,6 +97,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
return new DefaultSkinComponentsContainer(container =>
{
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
if (combo != null)
{
@ -102,9 +105,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
combo.Origin = Anchor.Centre;
combo.Y = this.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0;
}
if (spectatorList != null)
{
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = new Vector2(10, -10);
}
})
{
new LegacyManiaComboCounter(),
new SpectatorList(),
};
}

View File

@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK;
@ -70,12 +71,24 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
Vector2 pos = new Vector2();
if (combo != null)
{
combo.Anchor = Anchor.BottomLeft;
combo.Origin = Anchor.BottomLeft;
combo.Scale = new Vector2(1.28f);
pos += new Vector2(10, -(combo.DrawHeight * 1.56f + 20) * combo.Scale.X);
}
if (spectatorList != null)
{
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = pos;
}
})
{
@ -83,6 +96,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
new LegacyDefaultComboCounter(),
new LegacyKeyCounterDisplay(),
new SpectatorList(),
}
};
}

View File

@ -71,6 +71,8 @@ namespace osu.Game.Tests.Skins
"Archives/modified-classic-20240724.osk",
// Covers skinnable mod display
"Archives/modified-default-20241207.osk",
// Covers skinnable spectator list
"Archives/modified-argon-20250116.osk",
};
/// <summary>

View File

@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == newTime);
AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == EditorBeatmap.SnapTime(newTime, null));
}
[Test]
@ -122,6 +122,8 @@ namespace osu.Game.Tests.Visual.Editing
[TestCase(true)]
public void TestCopyPaste(bool deselectAfterCopy)
{
const int paste_time = 2000;
var addedObject = new HitCircle { StartTime = 1000 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
@ -130,7 +132,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("copy hitobject", () => Editor.Copy());
AddStep("move forward in time", () => EditorClock.Seek(2000));
AddStep("move forward in time", () => EditorClock.Seek(paste_time));
if (deselectAfterCopy)
{
@ -144,7 +146,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("are two objects", () => EditorBeatmap.HitObjects.Count == 2);
AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == 2000);
AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == EditorBeatmap.SnapTime(paste_time, null));
AddUntilStep("timeline selection box is visible", () => Editor.ChildrenOfType<Timeline>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0);
AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0);

View File

@ -0,0 +1,82 @@
// 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 NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual.Editing
{
public partial class TestSceneEditorClipboardSnapping : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
private const double beat_length = 60_000 / 180.0; // 180 bpm
private const double timing_point_time = 1500;
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(timing_point_time, new TimingControlPoint { BeatLength = beat_length });
return new TestBeatmap(ruleset, false)
{
ControlPointInfo = controlPointInfo
};
}
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
[TestCase(4)]
[TestCase(6)]
[TestCase(8)]
[TestCase(12)]
[TestCase(16)]
public void TestPasteSnapping(int divisor)
{
const double paste_time = timing_point_time + 1271; // arbitrary timestamp that doesn't snap to the timing point at any divisor
var addedObjects = new HitObject[]
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 1200 },
};
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
AddStep("select added objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
AddStep("copy hitobjects", () => Editor.Copy());
AddStep($"set beat divisor to 1/{divisor}", () =>
{
var beatDivisor = (BindableBeatDivisor)Editor.Dependencies.Get(typeof(BindableBeatDivisor));
beatDivisor.SetArbitraryDivisor(divisor);
});
AddStep("move forward in time", () => EditorClock.Seek(paste_time));
AddAssert("not at snapped time", () => EditorClock.CurrentTime != EditorBeatmap.SnapTime(EditorClock.CurrentTime, null));
AddStep("paste hitobjects", () => Editor.Paste());
AddAssert("first object is snapped", () => Precision.AlmostEquals(
EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime)!.StartTime,
EditorBeatmap.ControlPointInfo.GetClosestSnappedTime(paste_time, divisor)
));
AddAssert("duration between pasted objects is same", () =>
{
var firstObject = EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime)!;
var secondObject = EditorBeatmap.SelectedHitObjects.MaxBy(h => h.StartTime)!;
return Precision.AlmostEquals(secondObject.StartTime - firstObject.StartTime, addedObjects[1].StartTime - addedObjects[0].StartTime);
});
}
}
}

View File

@ -6,48 +6,74 @@ using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual.Spectator;
namespace osu.Game.Tests.Visual.Gameplay
{
[TestFixture]
public partial class TestSceneSpectatorList : OsuTestScene
{
private readonly BindableList<SpectatorList.Spectator> spectators = new BindableList<SpectatorList.Spectator>();
private readonly Bindable<LocalUserPlayingState> localUserPlayingState = new Bindable<LocalUserPlayingState>();
private int counter;
[Test]
public void TestBasics()
{
SpectatorList list = null!;
AddStep("create spectator list", () => Child = list = new SpectatorList
Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>();
GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState);
TestSpectatorClient client = new TestSpectatorClient();
AddStep("create spectator list", () =>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spectators = { BindTarget = spectators },
UserPlayingState = { BindTarget = localUserPlayingState }
Children = new Drawable[]
{
client,
new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies =
[
(typeof(GameplayState), gameplayState),
(typeof(SpectatorClient), client)
],
Child = list = new SpectatorList
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
};
});
AddStep("start playing", () => localUserPlayingState.Value = LocalUserPlayingState.Playing);
AddStep("start playing", () => playingState.Value = LocalUserPlayingState.Playing);
AddRepeatStep("add a user", () =>
{
int id = Interlocked.Increment(ref counter);
spectators.Add(new SpectatorList.Spectator(id, $"User {id}"));
((ISpectatorClient)client).UserStartedWatching([
new SpectatorUser
{
OnlineID = id,
Username = $"User {id}"
}
]);
}, 10);
AddRepeatStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count)), 5);
AddRepeatStep("remove random user", () => ((ISpectatorClient)client).UserEndedWatching(client.WatchingUsers[RNG.Next(client.WatchingUsers.Count)].OnlineID), 5);
AddStep("change font to venera", () => list.Font.Value = Typeface.Venera);
AddStep("change font to torus", () => list.Font.Value = Typeface.Torus);
AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1));
AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break);
AddStep("stop playing", () => localUserPlayingState.Value = LocalUserPlayingState.NotPlaying);
AddStep("enter break", () => playingState.Value = LocalUserPlayingState.Break);
AddStep("stop playing", () => playingState.Value = LocalUserPlayingState.NotPlaying);
}
}
}

View File

@ -48,6 +48,7 @@ using osu.Game.Screens.Select.Carousel;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Screens.Select.Options;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Resources;
using osu.Game.Utils;
using osuTK;
using osuTK.Input;
@ -202,6 +203,38 @@ namespace osu.Game.Tests.Visual.Navigation
TextBox filterControlTextBox() => songSelect.ChildrenOfType<FilterControl.FilterControlTextBox>().Single();
}
[Test]
public void TestSongSelectRandomRewindButton()
{
Guid? originalSelection = null;
TestPlaySongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
AddStep("Add two beatmaps", () =>
{
Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo(8));
Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo(8));
});
AddUntilStep("wait for selected", () =>
{
originalSelection = Game.Beatmap.Value.BeatmapInfo.ID;
return !Game.Beatmap.IsDefault;
});
AddStep("hit random", () =>
{
InputManager.MoveMouseTo(Game.ChildrenOfType<FooterButtonRandom>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for selection changed", () => Game.Beatmap.Value.BeatmapInfo.ID, () => Is.Not.EqualTo(originalSelection));
AddStep("hit random rewind", () => InputManager.Click(MouseButton.Right));
AddUntilStep("wait for selection reverted", () => Game.Beatmap.Value.BeatmapInfo.ID, () => Is.EqualTo(originalSelection));
}
[Test]
public void TestSongSelectScrollHandling()
{

View File

@ -16,6 +16,7 @@ using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
@ -117,12 +118,11 @@ namespace osu.Game.Tests.Visual.SongSelect
}
}
},
stats = new OsuTextFlowContainer(cp => cp.Font = FrameworkFont.Regular.With())
stats = new OsuTextFlowContainer
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding(10),
TextAnchor = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
};
});
@ -258,16 +258,29 @@ namespace osu.Game.Tests.Visual.SongSelect
if (carousel.IsNull())
return;
stats.Text = $"""
store
sets: {beatmapSets.Count}
beatmaps: {beatmapCount}
carousel:
sorting: {carousel.IsFiltering}
tracked: {carousel.ItemsTracked}
displayable: {carousel.DisplayableItems}
displayed: {carousel.VisibleItems}
""";
stats.Clear();
createHeader("beatmap store");
stats.AddParagraph($"""
sets: {beatmapSets.Count}
beatmaps: {beatmapCount}
""");
createHeader("carousel");
stats.AddParagraph($"""
sorting: {carousel.IsFiltering}
tracked: {carousel.ItemsTracked}
displayable: {carousel.DisplayableItems}
displayed: {carousel.VisibleItems}
selected: {carousel.CurrentSelection}
""");
void createHeader(string text)
{
stats.AddParagraph(string.Empty);
stats.AddParagraph(text, cp =>
{
cp.Font = cp.Font.With(size: 18, weight: FontWeight.Bold);
});
}
}
}
}

View File

@ -83,6 +83,40 @@ namespace osu.Game.Tests.Visual.UserInterface
waitForCompletion();
}
[Test]
public void TestNormalDoesForwardToOverlay()
{
SimpleNotification notification = null!;
AddStep(@"simple #1", () => notificationOverlay.Post(notification = new SimpleNotification
{
Text = @"This shouldn't annoy you too much",
Transient = false,
}));
AddAssert("notification in toast tray", () => notification.IsInToastTray, () => Is.True);
AddUntilStep("wait for dismissed", () => notification.IsInToastTray, () => Is.False);
checkDisplayedCount(1);
}
[Test]
public void TestTransientDoesNotForwardToOverlay()
{
SimpleNotification notification = null!;
AddStep(@"simple #1", () => notificationOverlay.Post(notification = new SimpleNotification
{
Text = @"This shouldn't annoy you too much",
Transient = true,
}));
AddAssert("notification in toast tray", () => notification.IsInToastTray, () => Is.True);
AddUntilStep("wait for dismissed", () => notification.IsInToastTray, () => Is.False);
checkDisplayedCount(0);
}
[Test]
public void TestForwardWithFlingRight()
{
@ -634,12 +668,18 @@ namespace osu.Game.Tests.Visual.UserInterface
private partial class BackgroundNotification : SimpleNotification
{
public override bool IsImportant => false;
public BackgroundNotification()
{
IsImportant = false;
}
}
private partial class BackgroundProgressNotification : ProgressNotification
{
public override bool IsImportant => false;
public BackgroundProgressNotification()
{
IsImportant = false;
}
}
}
}

View File

@ -113,6 +113,31 @@ namespace osu.Game.Beatmaps
return queryCacheVersion2(db, beatmapInfo, out onlineMetadata);
}
}
onlineMetadata = null;
return false;
}
catch (SqliteException sqliteException)
{
onlineMetadata = null;
// There have been cases where the user's local database is corrupt.
// Let's attempt to identify these cases and re-initialise the local cache.
switch (sqliteException.SqliteErrorCode)
{
case 26: // SQLITE_NOTADB
case 11: // SQLITE_CORRUPT
// only attempt purge & re-download if there is no other refetch in progress
if (cacheDownloadRequest != null)
return false;
tryPurgeCache();
prepareLocalCache();
return false;
}
logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo} failed with unhandled sqlite error {sqliteException}.");
return false;
}
catch (Exception ex)
{
@ -120,9 +145,22 @@ namespace osu.Game.Beatmaps
onlineMetadata = null;
return false;
}
}
onlineMetadata = null;
return false;
private void tryPurgeCache()
{
log(@"Local metadata cache is corrupted; attempting purge.");
try
{
File.Delete(storage.GetFullPath(cache_database_name));
}
catch (Exception ex)
{
log($@"Failed to purge local metadata cache: {ex}");
}
log(@"Local metadata cache purged due to corruption.");
}
private SqliteConnection getConnection() =>

View File

@ -131,8 +131,6 @@ namespace osu.Game.Database
private partial class DownloadNotification : ProgressNotification
{
public override bool IsImportant => false;
protected override Notification CreateCompletionNotification() => new SilencedProgressCompletionNotification
{
Activated = CompletionClickAction,
@ -141,7 +139,10 @@ namespace osu.Game.Database
private partial class SilencedProgressCompletionNotification : ProgressCompletionNotification
{
public override bool IsImportant => false;
public SilencedProgressCompletionNotification()
{
IsImportant = false;
}
}
}
}

View File

@ -11,7 +11,6 @@ using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Extensions;
@ -97,8 +96,9 @@ namespace osu.Game.Database
/// 44 2024-11-22 Removed several properties from BeatmapInfo which did not need to be persisted to realm.
/// 45 2024-12-23 Change beat snap divisor adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll, if not already changed by user.
/// 46 2024-12-26 Change beat snap divisor bindings to match stable directionality ¯\_(ツ)_/¯.
/// 47 2025-01-21 Remove right mouse button binding for absolute scroll. Never use mouse buttons (or scroll) for global actions.
/// </summary>
private const int schema_version = 46;
private const int schema_version = 47;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@ -413,18 +413,7 @@ namespace osu.Game.Database
/// Compact this realm.
/// </summary>
/// <returns></returns>
public bool Compact()
{
try
{
return Realm.Compact(getConfiguration());
}
// Catch can be removed along with entity framework. Is specifically to allow a failure message to arrive to the user (see similar catches in EFToRealmMigrator).
catch (AggregateException ae) when (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && ae.Flatten().InnerException is TypeInitializationException)
{
return true;
}
}
public bool Compact() => Realm.Compact(getConfiguration());
/// <summary>
/// Run work on realm with a return value.
@ -720,11 +709,6 @@ namespace osu.Game.Database
return Realm.GetInstance(getConfiguration());
}
// Catch can be removed along with entity framework. Is specifically to allow a failure message to arrive to the user (see similar catches in EFToRealmMigrator).
catch (AggregateException ae) when (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && ae.Flatten().InnerException is TypeInitializationException)
{
return Realm.GetInstance();
}
finally
{
if (tookSemaphoreLock)
@ -1239,6 +1223,17 @@ namespace osu.Game.Database
break;
}
case 47:
{
var keyBindings = migration.NewRealm.All<RealmKeyBinding>();
var existingBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.AbsoluteScrollSongList);
if (existingBinding != null && existingBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.MouseRight }))
migration.NewRealm.Remove(existingBinding);
break;
}
}
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");

View File

@ -14,7 +14,6 @@ using osu.Framework.Input.Events;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osuTK;
using osuTK.Input;
namespace osu.Game.Graphics.UserInterfaceV2
{
@ -75,14 +74,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
samplePopOut?.Play();
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Key == Key.Escape)
return false; // disable the framework-level handling of escape key for conformity (we use GlobalAction.Back).
return base.OnKeyDown(e);
}
public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat)

View File

@ -205,7 +205,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods),
new KeyBinding(new[] { InputKey.Control, InputKey.Up }, GlobalAction.IncreaseModSpeed),
new KeyBinding(new[] { InputKey.Control, InputKey.Down }, GlobalAction.DecreaseModSpeed),
new KeyBinding(new[] { InputKey.MouseRight }, GlobalAction.AbsoluteScrollSongList),
new KeyBinding(InputKey.None, GlobalAction.AbsoluteScrollSongList),
};
private static IEnumerable<KeyBinding> audioControlKeyBindings => new[]

View File

@ -169,6 +169,8 @@ namespace osu.Game.Online
notifications.Post(new SimpleNotification
{
Transient = true,
IsImportant = false,
Icon = FontAwesome.Solid.UserPlus,
Text = $"Online: {string.Join(@", ", onlineAlertQueue.Select(u => u.Username))}",
IconColour = colours.Green,
@ -204,6 +206,8 @@ namespace osu.Game.Online
notifications.Post(new SimpleNotification
{
Transient = true,
IsImportant = false,
Icon = FontAwesome.Solid.UserMinus,
Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}",
IconColour = colours.Red

View File

@ -37,5 +37,17 @@ namespace osu.Game.Online.Spectator
/// <param name="userId">The ID of the user who achieved the score.</param>
/// <param name="scoreId">The ID of the score.</param>
Task UserScoreProcessed(int userId, long scoreId);
/// <summary>
/// Signals that another user has <see cref="ISpectatorServer.StartWatchingUser">started watching this client</see>.
/// </summary>
/// <param name="user">The information about the user who started watching.</param>
Task UserStartedWatching(SpectatorUser[] user);
/// <summary>
/// Signals that another user has <see cref="ISpectatorServer.EndWatchingUser">ended watching this client</see>
/// </summary>
/// <param name="userId">The ID of the user who ended watching.</param>
Task UserEndedWatching(int userId);
}
}

View File

@ -42,6 +42,8 @@ namespace osu.Game.Online.Spectator
connection.On<int, FrameDataBundle>(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames);
connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying);
connection.On<int, long>(nameof(ISpectatorClient.UserScoreProcessed), ((ISpectatorClient)this).UserScoreProcessed);
connection.On<SpectatorUser[]>(nameof(ISpectatorClient.UserStartedWatching), ((ISpectatorClient)this).UserStartedWatching);
connection.On<int>(nameof(ISpectatorClient.UserEndedWatching), ((ISpectatorClient)this).UserEndedWatching);
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested);
};

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Development;
@ -36,10 +37,16 @@ namespace osu.Game.Online.Spectator
public abstract IBindable<bool> IsConnected { get; }
/// <summary>
/// The states of all users currently being watched.
/// The states of all users currently being watched by the local user.
/// </summary>
[UsedImplicitly] // Marked virtual due to mock use in testing
public virtual IBindableDictionary<int, SpectatorState> WatchedUserStates => watchedUserStates;
/// <summary>
/// All users who are currently watching the local user.
/// </summary>
public IBindableList<SpectatorUser> WatchingUsers => watchingUsers;
/// <summary>
/// A global list of all players currently playing.
/// </summary>
@ -53,6 +60,7 @@ namespace osu.Game.Online.Spectator
/// <summary>
/// Called whenever new frames arrive from the server.
/// </summary>
[UsedImplicitly] // Marked virtual due to mock use in testing
public virtual event Action<int, FrameDataBundle>? OnNewFrames;
/// <summary>
@ -82,6 +90,7 @@ namespace osu.Game.Online.Spectator
private readonly BindableDictionary<int, SpectatorState> watchedUserStates = new BindableDictionary<int, SpectatorState>();
private readonly BindableList<SpectatorUser> watchingUsers = new BindableList<SpectatorUser>();
private readonly BindableList<int> playingUsers = new BindableList<int>();
private readonly SpectatorState currentState = new SpectatorState();
@ -127,6 +136,7 @@ namespace osu.Game.Online.Spectator
{
playingUsers.Clear();
watchedUserStates.Clear();
watchingUsers.Clear();
}
}), true);
}
@ -179,6 +189,30 @@ namespace osu.Game.Online.Spectator
return Task.CompletedTask;
}
Task ISpectatorClient.UserStartedWatching(SpectatorUser[] users)
{
Schedule(() =>
{
foreach (var user in users)
{
if (!watchingUsers.Contains(user))
watchingUsers.Add(user);
}
});
return Task.CompletedTask;
}
Task ISpectatorClient.UserEndedWatching(int userId)
{
Schedule(() =>
{
watchingUsers.RemoveAll(u => u.OnlineID == userId);
});
return Task.CompletedTask;
}
Task IStatefulUserHubClient.DisconnectRequested()
{
Schedule(() => DisconnectInternal());

View File

@ -0,0 +1,39 @@
// 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 MessagePack;
using osu.Game.Users;
namespace osu.Game.Online.Spectator
{
[Serializable]
[MessagePackObject]
public class SpectatorUser : IUser, IEquatable<SpectatorUser>
{
[Key(0)]
public int OnlineID { get; set; }
[Key(1)]
public string Username { get; set; } = string.Empty;
[IgnoreMember]
public CountryCode CountryCode => CountryCode.Unknown;
[IgnoreMember]
public bool IsBot => false;
public bool Equals(SpectatorUser? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return OnlineID == other.OnlineID;
}
public override bool Equals(object? obj) => Equals(obj as SpectatorUser);
// ReSharper disable once NonReadonlyMemberInGetHashCode
public override int GetHashCode() => OnlineID;
}
}

View File

@ -41,7 +41,7 @@ namespace osu.Game.Overlays
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public Action<Notification>? ForwardNotificationToPermanentStore { get; set; }
public required Action<Notification> ForwardNotificationToPermanentStore { get; init; }
public int UnreadCount => Notifications.Count(n => !n.WasClosed && !n.Read);
@ -142,8 +142,15 @@ namespace osu.Game.Overlays
notification.MoveToOffset(new Vector2(400, 0), NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint);
notification.FadeOut(NotificationOverlay.TRANSITION_LENGTH, Easing.OutQuint).OnComplete(_ =>
{
if (notification.Transient)
{
notification.IsInToastTray = false;
notification.Close(false);
return;
}
RemoveInternal(notification, false);
ForwardNotificationToPermanentStore?.Invoke(notification);
ForwardNotificationToPermanentStore(notification);
notification.FadeIn(300, Easing.OutQuint);
});

View File

@ -34,9 +34,15 @@ namespace osu.Game.Overlays.Notifications
public abstract LocalisableString Text { get; set; }
/// <summary>
/// Whether this notification should forcefully display itself.
/// Important notifications display for longer, and announce themselves at an OS level (ie flashing the taskbar).
/// This defaults to <c>true</c>.
/// </summary>
public virtual bool IsImportant => true;
public bool IsImportant { get; init; } = true;
/// <summary>
/// Transient notifications only show as a toast, and do not linger in notification history.
/// </summary>
public bool Transient { get; init; }
/// <summary>
/// Run on user activating the notification. Return true to close.

View File

@ -191,8 +191,6 @@ namespace osu.Game.Overlays.Notifications
public override bool DisplayOnTop => false;
public override bool IsImportant => false;
private readonly ProgressBar progressBar;
private Color4 colourQueued;
private Color4 colourActive;
@ -206,6 +204,8 @@ namespace osu.Game.Overlays.Notifications
public ProgressNotification()
{
IsImportant = false;
Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14, weight: FontWeight.Medium))
{
AutoSizeAxes = Axes.Y,

View File

@ -149,7 +149,7 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
{
Enabled.Value = SelectedHitObject.Value != null;
if (SelectedHitObject.Value == null || SelectedHitObject.Value.ComboOffset == 0)
if (SelectedHitObject.Value == null || SelectedHitObject.Value.ComboOffset == 0 || ComboColours.Count <= 1)
{
BackgroundColour = colourProvider.Background3;
icon.Colour = BackgroundColour.Darken(0.5f);

View File

@ -111,25 +111,26 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override bool OnKeyDown(KeyDownEvent e)
{
// Until the keys below are global actions, this will prevent conflicts with "seek between sample points"
// which has a default of ctrl+shift+arrows.
if (e.ShiftPressed)
return false;
if (e.ControlPressed)
{
switch (e.Key)
{
case Key.Left:
nudgeSelection(new Vector2(-1, 0));
return true;
return nudgeSelection(new Vector2(-1, 0));
case Key.Right:
nudgeSelection(new Vector2(1, 0));
return true;
return nudgeSelection(new Vector2(1, 0));
case Key.Up:
nudgeSelection(new Vector2(0, -1));
return true;
return nudgeSelection(new Vector2(0, -1));
case Key.Down:
nudgeSelection(new Vector2(0, 1));
return true;
return nudgeSelection(new Vector2(0, 1));
}
}
@ -151,7 +152,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// Move the current selection spatially by the specified delta, in gamefield coordinates (ie. the same coordinates as the blueprints).
/// </summary>
/// <param name="delta"></param>
private void nudgeSelection(Vector2 delta)
private bool nudgeSelection(Vector2 delta)
{
if (!nudgeMovementActive)
{
@ -162,12 +163,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
var firstBlueprint = SelectionHandler.SelectedBlueprints.FirstOrDefault();
if (firstBlueprint == null)
return;
return false;
// convert to game space coordinates
delta = firstBlueprint.ToScreenSpace(delta) - firstBlueprint.ToScreenSpace(Vector2.Zero);
SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(firstBlueprint, delta));
return true;
}
private void updatePlacementNewCombo()

View File

@ -31,6 +31,9 @@ namespace osu.Game.Screens.Edit.Compose
[Resolved]
private IGameplaySettings globalGameplaySettings { get; set; }
[Resolved]
private IBeatSnapProvider beatSnapProvider { get; set; }
private Bindable<string> clipboard { get; set; }
private HitObjectComposer composer;
@ -150,7 +153,7 @@ namespace osu.Game.Screens.Edit.Compose
Debug.Assert(objects.Any());
double timeOffset = clock.CurrentTime - objects.Min(o => o.StartTime);
double timeOffset = beatSnapProvider.SnapTime(clock.CurrentTime) - objects.Min(o => o.StartTime);
foreach (var h in objects)
h.StartTime += timeOffset;

View File

@ -245,6 +245,15 @@ namespace osu.Game.Screens.Menu
if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed || e.SuperPressed)
return false;
if (e.Key >= Key.F1 && e.Key <= Key.F35)
return false;
switch (e.Key)
{
case Key.Escape:
return false;
}
if (triggerInitialOsuLogo())
return true;

View File

@ -166,11 +166,6 @@ namespace osu.Game.Screens.Play
protected override void PopOut() => this.FadeOut(TRANSITION_DURATION, Easing.In);
// Don't let mouse down events through the overlay or people can click circles while paused.
protected override bool OnMouseDown(MouseDownEvent e) => true;
protected override bool OnMouseMove(MouseMoveEvent e) => true;
protected void AddButton(LocalisableString text, Color4 colour, Action? action)
{
var button = new Button

View File

@ -69,6 +69,11 @@ namespace osu.Game.Screens.Play
private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>();
/// <summary>
/// The local user's playing state (whether actively playing, paused, or not playing due to watching a replay or similar).
/// </summary>
public IBindable<LocalUserPlayingState> PlayingState { get; } = new Bindable<LocalUserPlayingState>();
public GameplayState(
IBeatmap beatmap,
Ruleset ruleset,
@ -76,7 +81,8 @@ namespace osu.Game.Screens.Play
Score? score = null,
ScoreProcessor? scoreProcessor = null,
HealthProcessor? healthProcessor = null,
Storyboard? storyboard = null)
Storyboard? storyboard = null,
IBindable<LocalUserPlayingState>? localUserPlayingState = null)
{
Beatmap = beatmap;
Ruleset = ruleset;
@ -92,6 +98,9 @@ namespace osu.Game.Screens.Play
ScoreProcessor = scoreProcessor ?? ruleset.CreateScoreProcessor();
HealthProcessor = healthProcessor ?? ruleset.CreateHealthProcessor(beatmap.HitObjects[0].StartTime);
Storyboard = storyboard ?? new Storyboard();
if (localUserPlayingState != null)
PlayingState.BindTo(localUserPlayingState);
}
/// <summary>

View File

@ -15,18 +15,20 @@ using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
using osu.Game.Users;
using osu.Game.Localisation.HUD;
using osu.Game.Localisation.SkinComponents;
using osu.Game.Online.Spectator;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
{
public partial class SpectatorList : CompositeDrawable
public partial class SpectatorList : CompositeDrawable, ISerialisableDrawable
{
private const int max_spectators_displayed = 10;
public BindableList<Spectator> Spectators { get; } = new BindableList<Spectator>();
public BindableList<SpectatorUser> Spectators { get; } = new BindableList<SpectatorUser>();
public Bindable<LocalUserPlayingState> UserPlayingState { get; } = new Bindable<LocalUserPlayingState>();
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))]
@ -41,13 +43,20 @@ namespace osu.Game.Screens.Play.HUD
private FillFlowContainer<SpectatorListEntry> spectatorsFlow = null!;
private DrawablePool<SpectatorListEntry> pool = null!;
[Resolved]
private SpectatorClient client { get; set; } = null!;
[Resolved]
private GameplayState gameplayState { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
InternalChildren = new[]
{
Empty().With(t => t.Size = new Vector2(100, 50)),
mainFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
@ -76,6 +85,9 @@ namespace osu.Game.Screens.Play.HUD
{
base.LoadComplete();
((IBindableList<SpectatorUser>)Spectators).BindTo(client.WatchingUsers);
((IBindable<LocalUserPlayingState>)UserPlayingState).BindTo(gameplayState.PlayingState);
Spectators.BindCollectionChanged(onSpectatorsChanged, true);
UserPlayingState.BindValueChanged(_ => updateVisibility());
@ -94,7 +106,7 @@ namespace osu.Game.Screens.Play.HUD
{
for (int i = 0; i < e.NewItems!.Count; i++)
{
var spectator = (Spectator)e.NewItems![i]!;
var spectator = (SpectatorUser)e.NewItems![i]!;
int index = Math.Max(e.NewStartingIndex, 0) + i;
if (index >= max_spectators_displayed)
@ -143,7 +155,7 @@ namespace osu.Game.Screens.Play.HUD
}
}
private void addNewSpectatorToList(int i, Spectator spectator)
private void addNewSpectatorToList(int i, SpectatorUser spectator)
{
var entry = pool.Get(entry =>
{
@ -156,6 +168,7 @@ namespace osu.Game.Screens.Play.HUD
private void updateVisibility()
{
// We don't want to show spectators when we are watching a replay.
mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint);
}
@ -169,7 +182,7 @@ namespace osu.Game.Screens.Play.HUD
private partial class SpectatorListEntry : PoolableDrawable
{
public Bindable<Spectator> Current { get; } = new Bindable<Spectator>();
public Bindable<SpectatorUser> Current { get; } = new Bindable<SpectatorUser>();
private readonly BindableWithCurrent<LocalUserPlayingState> current = new BindableWithCurrent<LocalUserPlayingState>();
@ -233,10 +246,6 @@ namespace osu.Game.Screens.Play.HUD
}
}
public record Spectator(int OnlineID, string Username) : IUser
{
public CountryCode CountryCode => CountryCode.Unknown;
public bool IsBot => false;
}
public bool UsesFixedAnchor { get; set; }
}
}

View File

@ -261,7 +261,7 @@ namespace osu.Game.Screens.Play
Score.ScoreInfo.Ruleset = ruleset.RulesetInfo;
Score.ScoreInfo.Mods = gameplayMods;
dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor, HealthProcessor, Beatmap.Value.Storyboard));
dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor, HealthProcessor, Beatmap.Value.Storyboard, PlayingState));
var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin);
GameplayClockContainer.Add(new GameplayScrollWheelHandling());

View File

@ -663,8 +663,6 @@ namespace osu.Game.Screens.Play
private partial class MutedNotification : SimpleNotification
{
public override bool IsImportant => true;
public MutedNotification()
{
Text = NotificationsStrings.GameVolumeTooLow;
@ -716,8 +714,6 @@ namespace osu.Game.Screens.Play
private partial class BatteryWarningNotification : SimpleNotification
{
public override bool IsImportant => true;
public BatteryWarningNotification()
{
Text = NotificationsStrings.BatteryLow;

View File

@ -1181,14 +1181,7 @@ namespace osu.Game.Screens.Select
switch (e.Action)
{
case GlobalAction.AbsoluteScrollSongList:
// The default binding for absolute scroll is right mouse button.
// To avoid conflicts with context menus, disallow absolute scroll completely if it looks like things will fall over.
if (e.CurrentState.Mouse.Buttons.Contains(MouseButton.Right)
&& GetContainingInputManager()!.HoveredDrawables.OfType<IHasContextMenu>().Any())
return false;
ScrollToAbsolutePosition(e.CurrentState.Mouse.Position);
absoluteScrolling = true;
beginAbsoluteScrolling(e);
return true;
}
@ -1200,11 +1193,32 @@ namespace osu.Game.Screens.Select
switch (e.Action)
{
case GlobalAction.AbsoluteScrollSongList:
absoluteScrolling = false;
endAbsoluteScrolling();
break;
}
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button == MouseButton.Right)
{
// To avoid conflicts with context menus, disallow absolute scroll if it looks like things will fall over.
if (GetContainingInputManager()!.HoveredDrawables.OfType<IHasContextMenu>().Any())
return false;
beginAbsoluteScrolling(e);
}
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
if (e.Button == MouseButton.Right)
endAbsoluteScrolling();
base.OnMouseUp(e);
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
if (absoluteScrolling)
@ -1216,6 +1230,14 @@ namespace osu.Game.Screens.Select
return base.OnMouseMove(e);
}
private void beginAbsoluteScrolling(UIEvent e)
{
ScrollToAbsolutePosition(e.CurrentState.Mouse.Position);
absoluteScrolling = true;
}
private void endAbsoluteScrolling() => absoluteScrolling = false;
#endregion
protected override ScrollbarContainer CreateScrollbar(Direction direction)

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -14,7 +13,6 @@ using osu.Framework.Graphics.Pooling;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Select;
namespace osu.Game.Screens.SelectV2
@ -93,14 +91,8 @@ namespace osu.Game.Screens.SelectV2
public void Filter(FilterCriteria criteria)
{
Criteria = criteria;
FilterAsync().FireAndForget();
}
protected override async Task FilterAsync()
{
loading.Show();
await base.FilterAsync().ConfigureAwait(true);
loading.Hide();
FilterAsync().ContinueWith(_ => Schedule(() => loading.Hide()));
}
}
}

View File

@ -30,10 +30,7 @@ namespace osu.Game.Screens.SelectV2
/// </summary>
public abstract partial class Carousel<T> : CompositeDrawable
{
/// <summary>
/// A collection of filters which should be run each time a <see cref="FilterAsync"/> is executed.
/// </summary>
protected IEnumerable<ICarouselFilter> Filters { get; init; } = Enumerable.Empty<ICarouselFilter>();
#region Properties and methods for external usage
/// <summary>
/// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it.
@ -75,22 +72,13 @@ namespace osu.Game.Screens.SelectV2
/// <summary>
/// The number of carousel items currently in rotation for display.
/// </summary>
public int DisplayableItems => displayedCarouselItems?.Count ?? 0;
public int DisplayableItems => carouselItems?.Count ?? 0;
/// <summary>
/// The number of items currently actualised into drawables.
/// </summary>
public int VisibleItems => scroll.Panels.Count;
/// <summary>
/// All items which are to be considered for display in this carousel.
/// Mutating this list will automatically queue a <see cref="FilterAsync"/>.
/// </summary>
/// <remarks>
/// Note that an <see cref="ICarouselFilter"/> may add new items which are displayed but not tracked in this list.
/// </remarks>
protected readonly BindableList<T> Items = new BindableList<T>();
/// <summary>
/// The currently selected model.
/// </summary>
@ -114,20 +102,31 @@ namespace osu.Game.Screens.SelectV2
}
}
private List<CarouselItem>? displayedCarouselItems;
#endregion
private readonly CarouselScrollContainer scroll;
#region Properties and methods concerning implementations
protected Carousel()
{
InternalChild = scroll = new CarouselScrollContainer
{
RelativeSizeAxes = Axes.Both,
Masking = false,
};
/// <summary>
/// A collection of filters which should be run each time a <see cref="FilterAsync"/> is executed.
/// </summary>
/// <remarks>
/// Implementations should add all required filters as part of their initialisation.
///
/// Importantly, each filter is sequentially run in the order provided.
/// Each filter receives the output of the previous filter.
///
/// A filter may add, mutate or remove items.
/// </remarks>
protected IEnumerable<ICarouselFilter> Filters { get; init; } = Enumerable.Empty<ICarouselFilter>();
Items.BindCollectionChanged((_, _) => FilterAsync());
}
/// <summary>
/// All items which are to be considered for display in this carousel.
/// Mutating this list will automatically queue a <see cref="FilterAsync"/>.
/// </summary>
/// <remarks>
/// Note that an <see cref="ICarouselFilter"/> may add new items which are displayed but not tracked in this list.
/// </remarks>
protected readonly BindableList<T> Items = new BindableList<T>();
/// <summary>
/// Queue an asynchronous filter operation.
@ -151,8 +150,29 @@ namespace osu.Game.Screens.SelectV2
/// <returns>A <see cref="CarouselItem"/> representing the model.</returns>
protected abstract CarouselItem CreateCarouselItemForModel(T model);
#endregion
#region Initialisation
private readonly CarouselScrollContainer scroll;
protected Carousel()
{
InternalChild = scroll = new CarouselScrollContainer
{
RelativeSizeAxes = Axes.Both,
Masking = false,
};
Items.BindCollectionChanged((_, _) => FilterAsync());
}
#endregion
#region Filtering and display preparation
private List<CarouselItem>? carouselItems;
private Task filterTask = Task.CompletedTask;
private CancellationTokenSource cancellationSource = new CancellationTokenSource();
@ -202,7 +222,7 @@ namespace osu.Game.Screens.SelectV2
return;
log("Items ready for display");
displayedCarouselItems = items.ToList();
carouselItems = items.ToList();
displayedRange = null;
updateSelection();
@ -233,9 +253,9 @@ namespace osu.Game.Screens.SelectV2
{
currentSelectionCarouselItem = null;
if (displayedCarouselItems == null) return;
if (carouselItems == null) return;
foreach (var item in displayedCarouselItems)
foreach (var item in carouselItems)
{
bool isSelected = item.Model == currentSelection;
@ -286,7 +306,7 @@ namespace osu.Game.Screens.SelectV2
{
base.Update();
if (displayedCarouselItems == null)
if (carouselItems == null)
return;
var range = getDisplayRange();
@ -336,15 +356,15 @@ namespace osu.Game.Screens.SelectV2
private DisplayRange getDisplayRange()
{
Debug.Assert(displayedCarouselItems != null);
Debug.Assert(carouselItems != null);
// Find index range of all items that should be on-screen
carouselBoundsItem.CarouselYPosition = visibleUpperBound - DistanceOffscreenToPreload;
int firstIndex = displayedCarouselItems.BinarySearch(carouselBoundsItem);
int firstIndex = carouselItems.BinarySearch(carouselBoundsItem);
if (firstIndex < 0) firstIndex = ~firstIndex;
carouselBoundsItem.CarouselYPosition = visibleBottomBound + DistanceOffscreenToPreload;
int lastIndex = displayedCarouselItems.BinarySearch(carouselBoundsItem);
int lastIndex = carouselItems.BinarySearch(carouselBoundsItem);
if (lastIndex < 0) lastIndex = ~lastIndex;
firstIndex = Math.Max(0, firstIndex - 1);
@ -355,11 +375,11 @@ namespace osu.Game.Screens.SelectV2
private void updateDisplayedRange(DisplayRange range)
{
Debug.Assert(displayedCarouselItems != null);
Debug.Assert(carouselItems != null);
List<CarouselItem> toDisplay = range.Last - range.First == 0
? new List<CarouselItem>()
: displayedCarouselItems.GetRange(range.First, range.Last - range.First + 1);
: carouselItems.GetRange(range.First, range.Last - range.First + 1);
// Iterate over all panels which are already displayed and figure which need to be displayed / removed.
foreach (var panel in scroll.Panels)
@ -395,9 +415,9 @@ namespace osu.Game.Screens.SelectV2
// Update the total height of all items (to make the scroll container scrollable through the full height even though
// most items are not displayed / loaded).
if (displayedCarouselItems.Count > 0)
if (carouselItems.Count > 0)
{
var lastItem = displayedCarouselItems[^1];
var lastItem = carouselItems[^1];
scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight + visibleHalfHeight));
}
else
@ -493,15 +513,7 @@ namespace osu.Game.Screens.SelectV2
switch (e.Action)
{
case GlobalAction.AbsoluteScrollSongList:
// The default binding for absolute scroll is right mouse button.
// To avoid conflicts with context menus, disallow absolute scroll completely if it looks like things will fall over.
if (e.CurrentState.Mouse.Buttons.Contains(MouseButton.Right)
&& GetContainingInputManager()!.HoveredDrawables.OfType<IHasContextMenu>().Any())
return false;
ScrollToAbsolutePosition(e.CurrentState.Mouse.Position);
absoluteScrolling = true;
beginAbsoluteScrolling(e);
return true;
}
@ -513,11 +525,32 @@ namespace osu.Game.Screens.SelectV2
switch (e.Action)
{
case GlobalAction.AbsoluteScrollSongList:
absoluteScrolling = false;
endAbsoluteScrolling();
break;
}
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button == MouseButton.Right)
{
// To avoid conflicts with context menus, disallow absolute scroll if it looks like things will fall over.
if (GetContainingInputManager()!.HoveredDrawables.OfType<IHasContextMenu>().Any())
return false;
beginAbsoluteScrolling(e);
}
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
if (e.Button == MouseButton.Right)
endAbsoluteScrolling();
base.OnMouseUp(e);
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
if (absoluteScrolling)
@ -529,6 +562,14 @@ namespace osu.Game.Screens.SelectV2
return base.OnMouseMove(e);
}
private void beginAbsoluteScrolling(UIEvent e)
{
ScrollToAbsolutePosition(e.CurrentState.Mouse.Position);
absoluteScrolling = true;
}
private void endAbsoluteScrolling() => absoluteScrolling = false;
#endregion
}

View File

@ -7,7 +7,6 @@ using JetBrains.Annotations;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Beatmaps.Formats;
@ -110,15 +109,37 @@ namespace osu.Game.Skinning
case GlobalSkinnableContainers.MainHUDComponents:
if (containerLookup.Ruleset != null)
{
return new Container
return new DefaultSkinComponentsContainer(container =>
{
var comboCounter = container.OfType<ArgonComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
Vector2 pos = new Vector2(36, -66);
if (comboCounter != null)
{
comboCounter.Position = pos;
pos -= new Vector2(0, comboCounter.DrawHeight * 1.4f + 20);
}
if (spectatorList != null)
spectatorList.Position = pos;
})
{
RelativeSizeAxes = Axes.Both,
Child = new ArgonComboCounter
Children = new Drawable[]
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Position = new Vector2(36, -66),
Scale = new Vector2(1.3f),
new ArgonComboCounter
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Scale = new Vector2(1.3f),
},
new SpectatorList
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
}
},
};
}

View File

@ -367,16 +367,29 @@ namespace osu.Game.Skinning
return new DefaultSkinComponentsContainer(container =>
{
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
Vector2 pos = new Vector2();
if (combo != null)
{
combo.Anchor = Anchor.BottomLeft;
combo.Origin = Anchor.BottomLeft;
combo.Scale = new Vector2(1.28f);
pos += new Vector2(10, -(combo.DrawHeight * 1.56f + 20) * combo.Scale.X);
}
if (spectatorList != null)
{
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = pos;
}
})
{
new LegacyDefaultComboCounter()
new LegacyDefaultComboCounter(),
new SpectatorList(),
};
}

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Beatmaps.Formats;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.IO;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
@ -90,6 +91,7 @@ namespace osu.Game.Skinning
var ppCounter = container.OfType<PerformancePointsCounter>().FirstOrDefault();
var songProgress = container.OfType<DefaultSongProgress>().FirstOrDefault();
var keyCounter = container.OfType<DefaultKeyCounterDisplay>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
if (score != null)
{
@ -142,17 +144,26 @@ namespace osu.Game.Skinning
}
}
const float padding = 10;
// Hard to find this at runtime, so taken from the most expanded state during replay.
const float song_progress_offset_height = 73;
if (songProgress != null && keyCounter != null)
{
const float padding = 10;
// Hard to find this at runtime, so taken from the most expanded state during replay.
const float song_progress_offset_height = 73;
keyCounter.Anchor = Anchor.BottomRight;
keyCounter.Origin = Anchor.BottomRight;
keyCounter.Position = new Vector2(-padding, -(song_progress_offset_height + padding));
}
if (spectatorList != null)
{
spectatorList.Font.Value = Typeface.Venera;
spectatorList.HeaderColour.Value = new OsuColour().BlueLighter;
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = new Vector2(padding, -(song_progress_offset_height + padding));
}
})
{
Children = new Drawable[]
@ -165,7 +176,8 @@ namespace osu.Game.Skinning
new DefaultKeyCounterDisplay(),
new BarHitErrorMeter(),
new BarHitErrorMeter(),
new TrianglesPerformancePointsCounter()
new TrianglesPerformancePointsCounter(),
new SpectatorList(),
}
};