mirror of
https://github.com/ppy/osu.git
synced 2025-02-08 00:53:21 +08:00
Merge branch 'master' into pr
This commit is contained in:
commit
13fa49d5b5
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.129.1" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.204.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -2,10 +2,13 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -20,12 +23,23 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
||||
[Resolved]
|
||||
private OsuHitObjectComposer? composer { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private EditorClock? editorClock { get; set; }
|
||||
|
||||
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
|
||||
|
||||
public HitCirclePlacementBlueprint()
|
||||
: base(new HitCircle())
|
||||
{
|
||||
InternalChild = circlePiece = new HitCirclePiece();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
@ -53,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||
{
|
||||
var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime);
|
||||
result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition);
|
||||
result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition, limitedDistanceSnap.Value && editorClock != null ? editorClock.CurrentTime : null);
|
||||
if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult)
|
||||
result = gridSnapResult;
|
||||
result ??= new SnapResult(screenSpacePosition, fallbackTime);
|
||||
|
@ -21,6 +21,7 @@ using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -55,6 +56,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider distanceSnapProvider { get; set; }
|
||||
|
||||
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
|
||||
|
||||
public PathControlPointVisualiser(T hitObject, bool allowSelection)
|
||||
{
|
||||
this.hitObject = hitObject;
|
||||
@ -69,6 +72,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
@ -437,7 +446,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
|
||||
|
||||
var result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime);
|
||||
result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition);
|
||||
result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition, limitedDistanceSnap.Value ? oldStartTime : null);
|
||||
if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newHeadPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult)
|
||||
result = gridSnapResult;
|
||||
result ??= new SnapResult(newHeadPosition, oldStartTime);
|
||||
|
@ -5,10 +5,12 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@ -49,6 +51,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
[Resolved]
|
||||
private FreehandSliderToolboxGroup? freehandToolboxGroup { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private EditorClock? editorClock { get; set; }
|
||||
|
||||
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
|
||||
|
||||
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
|
||||
|
||||
protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
|
||||
@ -63,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
@ -74,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
};
|
||||
|
||||
state = SliderPlacementState.Initial;
|
||||
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -109,7 +117,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||
{
|
||||
var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime);
|
||||
result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition);
|
||||
result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition, limitedDistanceSnap.Value && editorClock != null ? editorClock.CurrentTime : null);
|
||||
if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult)
|
||||
result = gridSnapResult;
|
||||
result ??= new SnapResult(screenSpacePosition, fallbackTime);
|
||||
|
@ -3,7 +3,10 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
||||
@ -17,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class OsuBlueprintContainer : ComposeBlueprintContainer
|
||||
{
|
||||
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
|
||||
|
||||
public new OsuHitObjectComposer Composer => (OsuHitObjectComposer)base.Composer;
|
||||
|
||||
public OsuBlueprintContainer(OsuHitObjectComposer composer)
|
||||
@ -24,6 +29,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
|
||||
}
|
||||
|
||||
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new OsuSelectionHandler();
|
||||
|
||||
public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject)
|
||||
@ -58,15 +69,15 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
// The final movement position, relative to movementBlueprintOriginalPosition.
|
||||
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
|
||||
var referenceBlueprint = blueprints.First().blueprint;
|
||||
|
||||
// Retrieve a snapped position.
|
||||
var result = Composer.TrySnapToNearbyObjects(movePosition);
|
||||
result ??= Composer.TrySnapToDistanceGrid(movePosition);
|
||||
result ??= Composer.TrySnapToDistanceGrid(movePosition, limitedDistanceSnap.Value ? referenceBlueprint.Item.StartTime : null);
|
||||
if (Composer.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? movePosition, result?.Time) is SnapResult gridSnapResult)
|
||||
result = gridSnapResult;
|
||||
result ??= new SnapResult(movePosition, null);
|
||||
|
||||
var referenceBlueprint = blueprints.First().blueprint;
|
||||
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
|
||||
if (moved)
|
||||
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
|
||||
|
@ -250,13 +250,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition)
|
||||
public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition, double? fixedTime = null)
|
||||
{
|
||||
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null)
|
||||
return null;
|
||||
|
||||
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition), fixedTime);
|
||||
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, playfield);
|
||||
}
|
||||
|
||||
|
@ -181,7 +181,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
}
|
||||
}
|
||||
|
||||
public override (Vector2 position, double time) GetSnappedPosition(Vector2 screenSpacePosition)
|
||||
public override (Vector2 position, double time) GetSnappedPosition(Vector2 screenSpacePosition, double? fixedTime = null)
|
||||
=> (Vector2.Zero, 0);
|
||||
}
|
||||
|
||||
|
@ -60,14 +60,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
private void setUp()
|
||||
{
|
||||
AddStep("reset", () =>
|
||||
AddStep("create song select", () =>
|
||||
{
|
||||
Ruleset.Value = new OsuRuleset().RulesetInfo;
|
||||
Beatmap.SetDefault();
|
||||
SelectedMods.SetDefault();
|
||||
|
||||
LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value!));
|
||||
});
|
||||
|
||||
AddStep("create song select", () => LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value!)));
|
||||
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded);
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,7 @@ using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Match;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Resources;
|
||||
@ -271,7 +272,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddUntilStep("last playlist item selected", () =>
|
||||
{
|
||||
var lastItem = this.ChildrenOfType<DrawableRoomPlaylistItem>().Single(p => p.Item.ID == MultiplayerClient.ServerAPIRoom?.Playlist.Last().ID);
|
||||
var lastItem = this.ChildrenOfType<MultiplayerQueueList>()
|
||||
.Single()
|
||||
.ChildrenOfType<DrawableRoomPlaylistItem>()
|
||||
.Single(p => p.Item.ID == MultiplayerClient.ServerAPIRoom?.Playlist.Last().ID);
|
||||
return lastItem.IsSelectedItem;
|
||||
});
|
||||
}
|
||||
|
@ -308,6 +308,33 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddStep("set state: locally available", () => MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserWithStyle()
|
||||
{
|
||||
AddStep("add users", () =>
|
||||
{
|
||||
MultiplayerClient.AddUser(new APIUser
|
||||
{
|
||||
Id = 0,
|
||||
Username = "User 0",
|
||||
RulesetsStatistics = new Dictionary<string, UserStatistics>
|
||||
{
|
||||
{
|
||||
Ruleset.Value.ShortName,
|
||||
new UserStatistics { GlobalRank = RNG.Next(1, 100000), }
|
||||
}
|
||||
},
|
||||
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
});
|
||||
|
||||
MultiplayerClient.ChangeUserStyle(0, 259, 2);
|
||||
});
|
||||
|
||||
AddStep("set beatmap locally available", () => MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable()));
|
||||
AddStep("change user style to beatmap: 258, ruleset: 1", () => MultiplayerClient.ChangeUserStyle(0, 258, 1));
|
||||
AddStep("change user style to beatmap: null, ruleset: null", () => MultiplayerClient.ChangeUserStyle(0, null, null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModOverlap()
|
||||
{
|
||||
|
@ -21,6 +21,7 @@ using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
@ -53,7 +54,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
public virtual void SetUpSteps()
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
|
||||
@ -129,12 +130,61 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
});
|
||||
}
|
||||
|
||||
protected void SortBy(FilterCriteria criteria) => AddStep($"sort by {criteria.Sort}", () => Carousel.Filter(criteria));
|
||||
protected void SortBy(FilterCriteria criteria) => AddStep($"sort {criteria.Sort} group {criteria.Group}", () => Carousel.Filter(criteria));
|
||||
|
||||
protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType<ICarouselPanel>().Count(), () => Is.GreaterThan(0));
|
||||
protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False);
|
||||
protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target));
|
||||
|
||||
protected void SelectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down));
|
||||
protected void SelectPrevPanel() => AddStep("select prev panel", () => InputManager.Key(Key.Up));
|
||||
protected void SelectNextGroup() => AddStep("select next group", () => InputManager.Key(Key.Right));
|
||||
protected void SelectPrevGroup() => AddStep("select prev group", () => InputManager.Key(Key.Left));
|
||||
|
||||
protected void Select() => AddStep("select", () => InputManager.Key(Key.Enter));
|
||||
|
||||
protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null);
|
||||
protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null);
|
||||
|
||||
protected void WaitForGroupSelection(int group, int panel)
|
||||
{
|
||||
AddUntilStep($"selected is group{group} panel{panel}", () =>
|
||||
{
|
||||
var groupingFilter = Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single();
|
||||
|
||||
GroupDefinition g = groupingFilter.GroupItems.Keys.ElementAt(group);
|
||||
// offset by one because the group itself is included in the items list.
|
||||
CarouselItem item = groupingFilter.GroupItems[g].ElementAt(panel + 1);
|
||||
|
||||
return ReferenceEquals(Carousel.CurrentSelection, item.Model);
|
||||
});
|
||||
}
|
||||
|
||||
protected void WaitForSelection(int set, int? diff = null)
|
||||
{
|
||||
AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () =>
|
||||
{
|
||||
if (diff != null)
|
||||
return ReferenceEquals(Carousel.CurrentSelection, BeatmapSets[set].Beatmaps[diff.Value]);
|
||||
|
||||
return BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentSelection);
|
||||
});
|
||||
}
|
||||
|
||||
protected void ClickVisiblePanel<T>(int index)
|
||||
where T : Drawable
|
||||
{
|
||||
AddStep($"click panel at index {index}", () =>
|
||||
{
|
||||
Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single()
|
||||
.ChildrenOfType<T>()
|
||||
.Where(p => ((ICarouselPanel)p).Item?.IsVisible == true)
|
||||
.OrderBy(p => p.Y)
|
||||
.ElementAt(index)
|
||||
.TriggerClick();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add requested beatmap sets count to list.
|
||||
/// </summary>
|
||||
|
@ -32,6 +32,13 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
RemoveAllBeatmaps();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOffScreenLoading()
|
||||
{
|
||||
AddStep("disable masking", () => Scroll.Masking = false);
|
||||
AddStep("enable masking", () => Scroll.Masking = true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAddRemoveOneByOne()
|
||||
{
|
||||
@ -43,7 +50,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
public void TestSorting()
|
||||
{
|
||||
AddBeatmaps(10);
|
||||
SortBy(new FilterCriteria { Sort = SortMode.Difficulty });
|
||||
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
|
||||
SortBy(new FilterCriteria { Sort = SortMode.Artist });
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,158 @@
|
||||
// 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.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneBeatmapCarouselV2GroupSelection : BeatmapCarouselV2TestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
|
||||
CreateCarousel();
|
||||
|
||||
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOpenCloseGroupWithNoSelectionMouse()
|
||||
{
|
||||
AddBeatmaps(10, 5);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
CheckNoSelection();
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOpenCloseGroupWithNoSelectionKeyboard()
|
||||
{
|
||||
AddBeatmaps(10, 5);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
AddAssert("keyboard selected is expanded", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
|
||||
CheckNoSelection();
|
||||
|
||||
Select();
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddAssert("keyboard selected is collapsed", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
|
||||
CheckNoSelection();
|
||||
|
||||
GroupPanel? getKeyboardSelectedPanel() => Carousel.ChildrenOfType<GroupPanel>().SingleOrDefault(p => p.KeyboardSelected.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCarouselRemembersSelection()
|
||||
{
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextGroup();
|
||||
|
||||
object? selection = null;
|
||||
|
||||
AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model);
|
||||
|
||||
CheckHasSelection();
|
||||
AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null);
|
||||
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
|
||||
RemoveAllBeatmaps();
|
||||
AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null);
|
||||
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
CheckHasSelection();
|
||||
AddAssert("no drawable selection", getSelectedPanel, () => Is.Null);
|
||||
|
||||
AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!));
|
||||
|
||||
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
||||
AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddUntilStep("carousel item not visible", getSelectedPanel, () => Is.Null);
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddUntilStep("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
|
||||
BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType<BeatmapPanel>().SingleOrDefault(p => p.Selected.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGroupSelectionOnHeader()
|
||||
{
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(0, 0);
|
||||
|
||||
SelectPrevPanel();
|
||||
SelectPrevGroup();
|
||||
WaitForGroupSelection(2, 9);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKeyboardSelection()
|
||||
{
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
CheckNoSelection();
|
||||
|
||||
// open first group
|
||||
Select();
|
||||
CheckNoSelection();
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
WaitForGroupSelection(0, 0);
|
||||
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(0, 1);
|
||||
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(0, 2);
|
||||
|
||||
SelectPrevGroup();
|
||||
WaitForGroupSelection(0, 1);
|
||||
|
||||
SelectPrevGroup();
|
||||
WaitForGroupSelection(0, 0);
|
||||
|
||||
SelectPrevGroup();
|
||||
WaitForGroupSelection(2, 9);
|
||||
}
|
||||
}
|
||||
}
|
@ -22,10 +22,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
checkNoSelection();
|
||||
CheckNoSelection();
|
||||
|
||||
select();
|
||||
checkNoSelection();
|
||||
Select();
|
||||
CheckNoSelection();
|
||||
|
||||
AddStep("press down arrow", () => InputManager.PressKey(Key.Down));
|
||||
checkSelectionIterating(false);
|
||||
@ -39,8 +39,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddStep("release up arrow", () => InputManager.ReleaseKey(Key.Up));
|
||||
checkSelectionIterating(false);
|
||||
|
||||
select();
|
||||
checkHasSelection();
|
||||
Select();
|
||||
CheckHasSelection();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -52,7 +52,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
checkNoSelection();
|
||||
CheckNoSelection();
|
||||
|
||||
AddStep("press right arrow", () => InputManager.PressKey(Key.Right));
|
||||
checkSelectionIterating(true);
|
||||
@ -73,13 +73,13 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
selectNextGroup();
|
||||
SelectNextGroup();
|
||||
|
||||
object? selection = null;
|
||||
|
||||
AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model);
|
||||
|
||||
checkHasSelection();
|
||||
CheckHasSelection();
|
||||
AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null);
|
||||
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
|
||||
@ -89,13 +89,14 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
checkHasSelection();
|
||||
CheckHasSelection();
|
||||
AddAssert("no drawable selection", getSelectedPanel, () => Is.Null);
|
||||
|
||||
AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!));
|
||||
|
||||
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
||||
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
|
||||
BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType<BeatmapPanel>().SingleOrDefault(p => p.Selected.Value);
|
||||
}
|
||||
@ -108,10 +109,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddBeatmaps(total_set_count);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
selectNextGroup();
|
||||
waitForSelection(0, 0);
|
||||
selectPrevGroup();
|
||||
waitForSelection(total_set_count - 1, 0);
|
||||
SelectNextGroup();
|
||||
WaitForSelection(0, 0);
|
||||
SelectPrevGroup();
|
||||
WaitForSelection(total_set_count - 1, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -122,10 +123,25 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddBeatmaps(total_set_count);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
selectPrevGroup();
|
||||
waitForSelection(total_set_count - 1, 0);
|
||||
selectNextGroup();
|
||||
waitForSelection(0, 0);
|
||||
SelectPrevGroup();
|
||||
WaitForSelection(total_set_count - 1, 0);
|
||||
SelectNextGroup();
|
||||
WaitForSelection(0, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGroupSelectionOnHeader()
|
||||
{
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextGroup();
|
||||
SelectNextGroup();
|
||||
WaitForSelection(1, 0);
|
||||
|
||||
SelectPrevPanel();
|
||||
SelectPrevGroup();
|
||||
WaitForSelection(0, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -134,71 +150,50 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
selectNextPanel();
|
||||
selectNextPanel();
|
||||
selectNextPanel();
|
||||
selectNextPanel();
|
||||
checkNoSelection();
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
CheckNoSelection();
|
||||
|
||||
select();
|
||||
waitForSelection(3, 0);
|
||||
Select();
|
||||
WaitForSelection(3, 0);
|
||||
|
||||
selectNextPanel();
|
||||
waitForSelection(3, 0);
|
||||
SelectNextPanel();
|
||||
WaitForSelection(3, 0);
|
||||
|
||||
select();
|
||||
waitForSelection(3, 1);
|
||||
Select();
|
||||
WaitForSelection(3, 1);
|
||||
|
||||
selectNextPanel();
|
||||
waitForSelection(3, 1);
|
||||
SelectNextPanel();
|
||||
WaitForSelection(3, 1);
|
||||
|
||||
select();
|
||||
waitForSelection(3, 2);
|
||||
Select();
|
||||
WaitForSelection(3, 2);
|
||||
|
||||
selectNextPanel();
|
||||
waitForSelection(3, 2);
|
||||
SelectNextPanel();
|
||||
WaitForSelection(3, 2);
|
||||
|
||||
select();
|
||||
waitForSelection(4, 0);
|
||||
Select();
|
||||
WaitForSelection(4, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmptyTraversal()
|
||||
{
|
||||
selectNextPanel();
|
||||
checkNoSelection();
|
||||
SelectNextPanel();
|
||||
CheckNoSelection();
|
||||
|
||||
selectNextGroup();
|
||||
checkNoSelection();
|
||||
SelectNextGroup();
|
||||
CheckNoSelection();
|
||||
|
||||
selectPrevPanel();
|
||||
checkNoSelection();
|
||||
SelectPrevPanel();
|
||||
CheckNoSelection();
|
||||
|
||||
selectPrevGroup();
|
||||
checkNoSelection();
|
||||
SelectPrevGroup();
|
||||
CheckNoSelection();
|
||||
}
|
||||
|
||||
private void waitForSelection(int set, int? diff = null)
|
||||
{
|
||||
AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () =>
|
||||
{
|
||||
if (diff != null)
|
||||
return ReferenceEquals(Carousel.CurrentSelection, BeatmapSets[set].Beatmaps[diff.Value]);
|
||||
|
||||
return BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentSelection);
|
||||
});
|
||||
}
|
||||
|
||||
private void selectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down));
|
||||
private void selectPrevPanel() => AddStep("select prev panel", () => InputManager.Key(Key.Up));
|
||||
private void selectNextGroup() => AddStep("select next group", () => InputManager.Key(Key.Right));
|
||||
private void selectPrevGroup() => AddStep("select prev group", () => InputManager.Key(Key.Left));
|
||||
|
||||
private void select() => AddStep("select", () => InputManager.Key(Key.Enter));
|
||||
|
||||
private void checkNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null);
|
||||
private void checkHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null);
|
||||
|
||||
private void checkSelectionIterating(bool isIterating)
|
||||
{
|
||||
object? selection = null;
|
||||
|
@ -6,6 +6,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
@ -27,87 +28,102 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new FillFlowContainer
|
||||
Child = new OsuScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 400,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(5),
|
||||
Padding = new MarginPadding(10),
|
||||
Children = new Drawable[]
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
new FormTextBox
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 400,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(5),
|
||||
Padding = new MarginPadding(10),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Caption = "Artist",
|
||||
HintText = "Poot artist here!",
|
||||
PlaceholderText = "Here is an artist",
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormTextBox
|
||||
{
|
||||
Caption = "Artist",
|
||||
HintText = "Poot artist here!",
|
||||
PlaceholderText = "Here is an artist",
|
||||
Current = { Disabled = true },
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormNumberBox(allowDecimals: true)
|
||||
{
|
||||
Caption = "Number",
|
||||
HintText = "Insert your favourite number",
|
||||
PlaceholderText = "Mine is 42!",
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormCheckBox
|
||||
{
|
||||
Caption = EditorSetupStrings.LetterboxDuringBreaks,
|
||||
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
|
||||
},
|
||||
new FormCheckBox
|
||||
{
|
||||
Caption = EditorSetupStrings.LetterboxDuringBreaks,
|
||||
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
|
||||
Current = { Disabled = true },
|
||||
},
|
||||
new FormSliderBar<float>
|
||||
{
|
||||
Caption = "Slider",
|
||||
Current = new BindableFloat
|
||||
new FormTextBox
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Value = 5,
|
||||
Precision = 0.1f,
|
||||
Caption = "Artist",
|
||||
HintText = "Poot artist here!",
|
||||
PlaceholderText = "Here is an artist",
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormEnumDropdown<CountdownType>
|
||||
{
|
||||
Caption = EditorSetupStrings.EnableCountdown,
|
||||
HintText = EditorSetupStrings.CountdownDescription,
|
||||
},
|
||||
new FormFileSelector
|
||||
{
|
||||
Caption = "File selector",
|
||||
PlaceholderText = "Select a file",
|
||||
},
|
||||
new FormBeatmapFileSelector(true)
|
||||
{
|
||||
Caption = "File selector with intermediate choice dialog",
|
||||
PlaceholderText = "Select a file",
|
||||
},
|
||||
new FormColourPalette
|
||||
{
|
||||
Caption = "Combo colours",
|
||||
Colours =
|
||||
new FormTextBox
|
||||
{
|
||||
Colour4.Red,
|
||||
Colour4.Green,
|
||||
Colour4.Blue,
|
||||
Colour4.Yellow,
|
||||
}
|
||||
Caption = "Artist",
|
||||
HintText = "Poot artist here!",
|
||||
PlaceholderText = "Here is an artist",
|
||||
Current = { Disabled = true },
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormNumberBox(allowDecimals: true)
|
||||
{
|
||||
Caption = "Number",
|
||||
HintText = "Insert your favourite number",
|
||||
PlaceholderText = "Mine is 42!",
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormCheckBox
|
||||
{
|
||||
Caption = EditorSetupStrings.LetterboxDuringBreaks,
|
||||
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
|
||||
},
|
||||
new FormCheckBox
|
||||
{
|
||||
Caption = EditorSetupStrings.LetterboxDuringBreaks,
|
||||
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
|
||||
Current = { Disabled = true },
|
||||
},
|
||||
new FormSliderBar<float>
|
||||
{
|
||||
Caption = "Slider",
|
||||
Current = new BindableFloat
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Value = 5,
|
||||
Precision = 0.1f,
|
||||
},
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormEnumDropdown<CountdownType>
|
||||
{
|
||||
Caption = EditorSetupStrings.EnableCountdown,
|
||||
HintText = EditorSetupStrings.CountdownDescription,
|
||||
},
|
||||
new FormFileSelector
|
||||
{
|
||||
Caption = "File selector",
|
||||
PlaceholderText = "Select a file",
|
||||
},
|
||||
new FormBeatmapFileSelector(true)
|
||||
{
|
||||
Caption = "File selector with intermediate choice dialog",
|
||||
PlaceholderText = "Select a file",
|
||||
},
|
||||
new FormColourPalette
|
||||
{
|
||||
Caption = "Combo colours",
|
||||
Colours =
|
||||
{
|
||||
Colour4.Red,
|
||||
Colour4.Green,
|
||||
Colour4.Blue,
|
||||
Colour4.Yellow,
|
||||
}
|
||||
},
|
||||
new FormButton
|
||||
{
|
||||
Caption = "No text in button",
|
||||
Action = () => { },
|
||||
},
|
||||
new FormButton
|
||||
{
|
||||
Caption = "Text in button which is pretty long and is very likely to wrap",
|
||||
ButtonText = "Foo the bar",
|
||||
Action = () => { },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -447,60 +447,31 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
private void addPathData(TextWriter writer, IHasPath pathData, Vector2 position)
|
||||
{
|
||||
PathType? lastType = null;
|
||||
|
||||
for (int i = 0; i < pathData.Path.ControlPoints.Count; i++)
|
||||
{
|
||||
PathControlPoint point = pathData.Path.ControlPoints[i];
|
||||
|
||||
// Note that lazer's encoding format supports specifying multiple curve types for a slider path, which is not supported by stable.
|
||||
// Backwards compatibility with stable is handled by `LegacyBeatmapExporter` and `BezierConverter.ConvertToModernBezier()`.
|
||||
if (point.Type != null)
|
||||
{
|
||||
// We've reached a new (explicit) segment!
|
||||
|
||||
// Explicit segments have a new format in which the type is injected into the middle of the control point string.
|
||||
// To preserve compatibility with osu-stable as much as possible, explicit segments with the same type are converted to use implicit segments by duplicating the control point.
|
||||
// One exception are consecutive perfect curves, which aren't supported in osu!stable and can lead to decoding issues if encoded as implicit segments
|
||||
bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECT_CURVE || i == pathData.Path.ControlPoints.Count - 1;
|
||||
|
||||
// Another exception to this is when the last two control points of the last segment were duplicated. This is not a scenario supported by osu!stable.
|
||||
// Lazer does not add implicit segments for the last two control points of _any_ explicit segment, so an explicit segment is forced in order to maintain consistency with the decoder.
|
||||
if (i > 1)
|
||||
switch (point.Type?.Type)
|
||||
{
|
||||
// We need to use the absolute control point position to determine equality, otherwise floating point issues may arise.
|
||||
Vector2 p1 = position + pathData.Path.ControlPoints[i - 1].Position;
|
||||
Vector2 p2 = position + pathData.Path.ControlPoints[i - 2].Position;
|
||||
case SplineType.BSpline:
|
||||
writer.Write(point.Type.Value.Degree > 0 ? $"B{point.Type.Value.Degree}|" : "B|");
|
||||
break;
|
||||
|
||||
if ((int)p1.X == (int)p2.X && (int)p1.Y == (int)p2.Y)
|
||||
needsExplicitSegment = true;
|
||||
}
|
||||
case SplineType.Catmull:
|
||||
writer.Write("C|");
|
||||
break;
|
||||
|
||||
if (needsExplicitSegment)
|
||||
{
|
||||
switch (point.Type?.Type)
|
||||
{
|
||||
case SplineType.BSpline:
|
||||
writer.Write(point.Type.Value.Degree > 0 ? $"B{point.Type.Value.Degree}|" : "B|");
|
||||
break;
|
||||
case SplineType.PerfectCurve:
|
||||
writer.Write("P|");
|
||||
break;
|
||||
|
||||
case SplineType.Catmull:
|
||||
writer.Write("C|");
|
||||
break;
|
||||
|
||||
case SplineType.PerfectCurve:
|
||||
writer.Write("P|");
|
||||
break;
|
||||
|
||||
case SplineType.Linear:
|
||||
writer.Write("L|");
|
||||
break;
|
||||
}
|
||||
|
||||
lastType = point.Type;
|
||||
}
|
||||
else
|
||||
{
|
||||
// New segment with the same type - duplicate the control point
|
||||
writer.Write(FormattableString.Invariant($"{position.X + point.Position.X}:{position.Y + point.Position.Y}|"));
|
||||
case SplineType.Linear:
|
||||
writer.Write("L|");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -203,6 +203,8 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: This is a touch expensive and can become an issue if being accessed every Update call.
|
||||
// Optimally we would not involve the async flow if things are already loaded.
|
||||
return loadBeatmapAsync().GetResultSafely();
|
||||
}
|
||||
catch (AggregateException ae)
|
||||
|
@ -120,18 +120,30 @@ namespace osu.Game.Database
|
||||
if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1
|
||||
&& hasPath.Path.ControlPoints[0].Type!.Value.Degree == null) continue;
|
||||
|
||||
var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints);
|
||||
|
||||
// Truncate control points to integer positions
|
||||
foreach (var pathControlPoint in newControlPoints)
|
||||
{
|
||||
pathControlPoint.Position = new Vector2(
|
||||
(float)Math.Floor(pathControlPoint.Position.X),
|
||||
(float)Math.Floor(pathControlPoint.Position.Y));
|
||||
}
|
||||
var convertedToBezier = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints);
|
||||
|
||||
hasPath.Path.ControlPoints.Clear();
|
||||
hasPath.Path.ControlPoints.AddRange(newControlPoints);
|
||||
|
||||
for (int i = 0; i < convertedToBezier.Count; i++)
|
||||
{
|
||||
var convertedPoint = convertedToBezier[i];
|
||||
|
||||
// Truncate control points to integer positions
|
||||
var position = new Vector2(
|
||||
(float)Math.Floor(convertedPoint.Position.X),
|
||||
(float)Math.Floor(convertedPoint.Position.Y));
|
||||
|
||||
// stable only supports a single curve type specification per slider.
|
||||
// we exploit the fact that the converted-to-Bézier path only has Bézier segments,
|
||||
// and thus we specify the Bézier curve type once ever at the start of the slider.
|
||||
hasPath.Path.ControlPoints.Add(new PathControlPoint(position, i == 0 ? PathType.BEZIER : null));
|
||||
|
||||
// however, the Bézier path as output by the converter has multiple segments.
|
||||
// `LegacyBeatmapEncoder` will attempt to encode this by emitting per-control-point curve type specs which don't do anything for stable.
|
||||
// instead, stable expects control points that start a segment to be present in the path twice in succession.
|
||||
if (convertedPoint.Type == PathType.BEZIER && i > 0)
|
||||
hasPath.Path.ControlPoints.Add(new PathControlPoint(position));
|
||||
}
|
||||
}
|
||||
|
||||
// Encode to legacy format
|
||||
|
189
osu.Game/Graphics/UserInterfaceV2/FormButton.cs
Normal file
189
osu.Game/Graphics/UserInterfaceV2/FormButton.cs
Normal file
@ -0,0 +1,189 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
public partial class FormButton : CompositeDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// Caption describing this button, displayed on the left of it.
|
||||
/// </summary>
|
||||
public LocalisableString Caption { get; init; }
|
||||
|
||||
public LocalisableString ButtonText { get; init; }
|
||||
|
||||
public Action? Action { get; init; }
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 50;
|
||||
|
||||
Masking = true;
|
||||
CornerRadius = 5;
|
||||
CornerExponent = 2.5f;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background5,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Left = 9,
|
||||
Right = 5,
|
||||
Vertical = 5,
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuTextFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Width = 0.45f,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Text = Caption,
|
||||
},
|
||||
new Button
|
||||
{
|
||||
Action = Action,
|
||||
Text = ButtonText,
|
||||
RelativeSizeAxes = ButtonText == default ? Axes.None : Axes.X,
|
||||
Width = ButtonText == default ? 90 : 0.45f,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateState();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
base.OnHoverLost(e);
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
BorderThickness = IsHovered ? 2 : 0;
|
||||
|
||||
if (IsHovered)
|
||||
BorderColour = colourProvider.Light4;
|
||||
}
|
||||
|
||||
public partial class Button : OsuButton
|
||||
{
|
||||
private TrianglesV2? triangles { get; set; }
|
||||
|
||||
protected override float HoverLayerFinalAlpha => 0;
|
||||
|
||||
private Color4? triangleGradientSecondColour;
|
||||
|
||||
public override Color4 BackgroundColour
|
||||
{
|
||||
get => base.BackgroundColour;
|
||||
set
|
||||
{
|
||||
base.BackgroundColour = value;
|
||||
triangleGradientSecondColour = BackgroundColour.Lighten(0.2f);
|
||||
updateColours();
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider overlayColourProvider)
|
||||
{
|
||||
DefaultBackgroundColour = overlayColourProvider.Colour3;
|
||||
triangleGradientSecondColour ??= overlayColourProvider.Colour1;
|
||||
|
||||
if (Text == default)
|
||||
{
|
||||
Add(new SpriteIcon
|
||||
{
|
||||
Icon = FontAwesome.Solid.ChevronRight,
|
||||
Size = new Vector2(16),
|
||||
Shadow = true,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Content.CornerRadius = 2;
|
||||
|
||||
Add(triangles = new TrianglesV2
|
||||
{
|
||||
Thickness = 0.02f,
|
||||
SpawnRatio = 0.6f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = float.MaxValue,
|
||||
});
|
||||
|
||||
updateColours();
|
||||
}
|
||||
|
||||
private void updateColours()
|
||||
{
|
||||
if (triangles == null)
|
||||
return;
|
||||
|
||||
Debug.Assert(triangleGradientSecondColour != null);
|
||||
|
||||
triangles.Colour = ColourInfo.GradientVertical(triangleGradientSecondColour.Value, BackgroundColour);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
Debug.Assert(triangleGradientSecondColour != null);
|
||||
|
||||
Background.FadeColour(triangleGradientSecondColour.Value, 300, Easing.OutQuint);
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
Background.FadeColour(BackgroundColour, 300, Easing.OutQuint);
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -24,6 +24,21 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString StartMatchWithCountdown(string humanReadableTime) => new TranslatableString(getKey(@"start_match_width_countdown"), @"Start match in {0}", humanReadableTime);
|
||||
|
||||
/// <summary>
|
||||
/// "Choose the mods which all players should play with."
|
||||
/// </summary>
|
||||
public static LocalisableString RequiredModsButtonTooltip => new TranslatableString(getKey(@"required_mods_button_tooltip"), @"Choose the mods which all players should play with.");
|
||||
|
||||
/// <summary>
|
||||
/// "Each player can choose their preferred mods from a selected list."
|
||||
/// </summary>
|
||||
public static LocalisableString FreeModsButtonTooltip => new TranslatableString(getKey(@"free_mods_button_tooltip"), @"Each player can choose their preferred mods from a selected list.");
|
||||
|
||||
/// <summary>
|
||||
/// "Each player can choose their preferred difficulty, ruleset and mods."
|
||||
/// </summary>
|
||||
public static LocalisableString FreestyleButtonTooltip => new TranslatableString(getKey(@"freestyle_button_tooltip"), @"Each player can choose their preferred difficulty, ruleset and mods.");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -95,6 +95,14 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// <param name="beatmapAvailability">The new beatmap availability state of the user.</param>
|
||||
Task UserBeatmapAvailabilityChanged(int userId, BeatmapAvailability beatmapAvailability);
|
||||
|
||||
/// <summary>
|
||||
/// Signals that a user in this room changed their style.
|
||||
/// </summary>
|
||||
/// <param name="userId">The ID of the user whose style changed.</param>
|
||||
/// <param name="beatmapId">The user's beatmap.</param>
|
||||
/// <param name="rulesetId">The user's ruleset.</param>
|
||||
Task UserStyleChanged(int userId, int? beatmapId, int? rulesetId);
|
||||
|
||||
/// <summary>
|
||||
/// Signals that a user in this room changed their local mods.
|
||||
/// </summary>
|
||||
|
@ -57,6 +57,13 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// <param name="newBeatmapAvailability">The proposed new beatmap availability state.</param>
|
||||
Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability);
|
||||
|
||||
/// <summary>
|
||||
/// Change the local user's style in the currently joined room.
|
||||
/// </summary>
|
||||
/// <param name="beatmapId">The beatmap.</param>
|
||||
/// <param name="rulesetId">The ruleset.</param>
|
||||
Task ChangeUserStyle(int? beatmapId, int? rulesetId);
|
||||
|
||||
/// <summary>
|
||||
/// Change the local user's mods in the currently joined room.
|
||||
/// </summary>
|
||||
|
@ -358,6 +358,8 @@ namespace osu.Game.Online.Multiplayer
|
||||
|
||||
public abstract Task DisconnectInternal();
|
||||
|
||||
public abstract Task ChangeUserStyle(int? beatmapId, int? rulesetId);
|
||||
|
||||
/// <summary>
|
||||
/// Change the local user's mods in the currently joined room.
|
||||
/// </summary>
|
||||
@ -653,6 +655,25 @@ namespace osu.Game.Online.Multiplayer
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task UserStyleChanged(int userId, int? beatmapId, int? rulesetId)
|
||||
{
|
||||
Scheduler.Add(() =>
|
||||
{
|
||||
var user = Room?.Users.SingleOrDefault(u => u.UserID == userId);
|
||||
|
||||
// errors here are not critical - user style is mostly for display.
|
||||
if (user == null)
|
||||
return;
|
||||
|
||||
user.BeatmapId = beatmapId;
|
||||
user.RulesetId = rulesetId;
|
||||
|
||||
RoomUpdated?.Invoke();
|
||||
}, false);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task UserModsChanged(int userId, IEnumerable<APIMod> mods)
|
||||
{
|
||||
Scheduler.Add(() =>
|
||||
|
@ -22,9 +22,6 @@ namespace osu.Game.Online.Multiplayer
|
||||
[Key(1)]
|
||||
public MultiplayerUserState State { get; set; } = MultiplayerUserState.Idle;
|
||||
|
||||
[Key(4)]
|
||||
public MatchUserState? MatchState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The availability state of the current beatmap.
|
||||
/// </summary>
|
||||
@ -37,6 +34,21 @@ namespace osu.Game.Online.Multiplayer
|
||||
[Key(3)]
|
||||
public IEnumerable<APIMod> Mods { get; set; } = Enumerable.Empty<APIMod>();
|
||||
|
||||
[Key(4)]
|
||||
public MatchUserState? MatchState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If not-null, a local override for this user's ruleset selection.
|
||||
/// </summary>
|
||||
[Key(5)]
|
||||
public int? RulesetId;
|
||||
|
||||
/// <summary>
|
||||
/// If not-null, a local override for this user's beatmap selection.
|
||||
/// </summary>
|
||||
[Key(6)]
|
||||
public int? BeatmapId;
|
||||
|
||||
[IgnoreMember]
|
||||
public APIUser? User { get; set; }
|
||||
|
||||
|
@ -60,6 +60,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
connection.On(nameof(IMultiplayerClient.GameplayStarted), ((IMultiplayerClient)this).GameplayStarted);
|
||||
connection.On<GameplayAbortReason>(nameof(IMultiplayerClient.GameplayAborted), ((IMultiplayerClient)this).GameplayAborted);
|
||||
connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady);
|
||||
connection.On<int, int?, int?>(nameof(IMultiplayerClient.UserStyleChanged), ((IMultiplayerClient)this).UserStyleChanged);
|
||||
connection.On<int, IEnumerable<APIMod>>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged);
|
||||
connection.On<int, BeatmapAvailability>(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged);
|
||||
connection.On<MatchRoomState>(nameof(IMultiplayerClient.MatchRoomStateChanged), ((IMultiplayerClient)this).MatchRoomStateChanged);
|
||||
@ -186,6 +187,16 @@ namespace osu.Game.Online.Multiplayer
|
||||
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeBeatmapAvailability), newBeatmapAvailability);
|
||||
}
|
||||
|
||||
public override Task ChangeUserStyle(int? beatmapId, int? rulesetId)
|
||||
{
|
||||
if (!IsConnected.Value)
|
||||
return Task.CompletedTask;
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
|
||||
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeUserStyle), beatmapId, rulesetId);
|
||||
}
|
||||
|
||||
public override Task ChangeUserMods(IEnumerable<APIMod> newMods)
|
||||
{
|
||||
if (!IsConnected.Value)
|
||||
|
@ -31,6 +31,7 @@ namespace osu.Game.Online.Rooms
|
||||
var req = base.CreateWebRequest();
|
||||
req.Method = HttpMethod.Post;
|
||||
req.AddParameter("version_hash", versionHash);
|
||||
req.AddParameter("beatmap_id", beatmapInfo.OnlineID.ToString(CultureInfo.InvariantCulture));
|
||||
req.AddParameter("beatmap_hash", beatmapInfo.MD5Hash);
|
||||
req.AddParameter("ruleset_id", rulesetId.ToString(CultureInfo.InvariantCulture));
|
||||
return req;
|
||||
|
@ -56,6 +56,12 @@ namespace osu.Game.Online.Rooms
|
||||
[Key(10)]
|
||||
public double StarRating { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset.
|
||||
/// </summary>
|
||||
[Key(11)]
|
||||
public bool Freestyle { get; set; }
|
||||
|
||||
[SerializationConstructor]
|
||||
public MultiplayerPlaylistItem()
|
||||
{
|
||||
|
@ -77,11 +77,14 @@ namespace osu.Game.Online.Rooms
|
||||
[CanBeNull]
|
||||
public MultiplayerScoresAround ScoresAround { get; set; }
|
||||
|
||||
public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, PlaylistItem playlistItem, [NotNull] BeatmapInfo beatmap)
|
||||
[JsonProperty("ruleset_id")]
|
||||
public int RulesetId { get; set; }
|
||||
|
||||
public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, [NotNull] BeatmapInfo beatmap)
|
||||
{
|
||||
var ruleset = rulesets.GetRuleset(playlistItem.RulesetID);
|
||||
var ruleset = rulesets.GetRuleset(RulesetId);
|
||||
if (ruleset == null)
|
||||
throw new InvalidOperationException($"Couldn't create score with unknown ruleset: {playlistItem.RulesetID}");
|
||||
throw new InvalidOperationException($"Couldn't create score with unknown ruleset: {RulesetId}");
|
||||
|
||||
var rulesetInstance = ruleset.CreateInstance();
|
||||
|
||||
@ -91,7 +94,7 @@ namespace osu.Game.Online.Rooms
|
||||
TotalScore = TotalScore,
|
||||
MaxCombo = MaxCombo,
|
||||
BeatmapInfo = beatmap,
|
||||
Ruleset = rulesets.GetRuleset(playlistItem.RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {playlistItem.RulesetID} not found locally"),
|
||||
Ruleset = ruleset,
|
||||
Passed = Passed,
|
||||
Statistics = Statistics,
|
||||
MaximumStatistics = MaximumStatistics,
|
||||
|
@ -67,6 +67,12 @@ namespace osu.Game.Online.Rooms
|
||||
set => Beatmap = new APIBeatmap { OnlineID = value };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset.
|
||||
/// </summary>
|
||||
[JsonProperty("freestyle")]
|
||||
public bool Freestyle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A beatmap representing this playlist item.
|
||||
/// In many cases, this will *not* contain any usable information apart from OnlineID.
|
||||
@ -101,6 +107,7 @@ namespace osu.Game.Online.Rooms
|
||||
PlayedAt = item.PlayedAt;
|
||||
RequiredMods = item.RequiredMods.ToArray();
|
||||
AllowedMods = item.AllowedMods.ToArray();
|
||||
Freestyle = item.Freestyle;
|
||||
}
|
||||
|
||||
public void MarkInvalid() => valid.Value = false;
|
||||
@ -120,18 +127,19 @@ namespace osu.Game.Online.Rooms
|
||||
|
||||
#endregion
|
||||
|
||||
public PlaylistItem With(Optional<long> id = default, Optional<IBeatmapInfo> beatmap = default, Optional<ushort?> playlistOrder = default)
|
||||
public PlaylistItem With(Optional<long> id = default, Optional<IBeatmapInfo> beatmap = default, Optional<ushort?> playlistOrder = default, Optional<int> ruleset = default)
|
||||
{
|
||||
return new PlaylistItem(beatmap.GetOr(Beatmap))
|
||||
{
|
||||
ID = id.GetOr(ID),
|
||||
OwnerID = OwnerID,
|
||||
RulesetID = RulesetID,
|
||||
RulesetID = ruleset.GetOr(RulesetID),
|
||||
Expired = Expired,
|
||||
PlaylistOrder = playlistOrder.GetOr(PlaylistOrder),
|
||||
PlayedAt = PlayedAt,
|
||||
AllowedMods = AllowedMods,
|
||||
RequiredMods = RequiredMods,
|
||||
Freestyle = Freestyle,
|
||||
valid = { Value = Valid.Value },
|
||||
};
|
||||
}
|
||||
@ -143,6 +151,7 @@ namespace osu.Game.Online.Rooms
|
||||
&& Expired == other.Expired
|
||||
&& PlaylistOrder == other.PlaylistOrder
|
||||
&& AllowedMods.SequenceEqual(other.AllowedMods)
|
||||
&& RequiredMods.SequenceEqual(other.RequiredMods);
|
||||
&& RequiredMods.SequenceEqual(other.RequiredMods)
|
||||
&& Freestyle == other.Freestyle;
|
||||
}
|
||||
}
|
||||
|
@ -16,9 +16,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
public abstract partial class CircularDistanceSnapGrid : DistanceSnapGrid
|
||||
{
|
||||
[Resolved]
|
||||
private EditorClock editorClock { get; set; } = null!;
|
||||
|
||||
protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null)
|
||||
: base(referenceObject, startPosition, startTime, endTime)
|
||||
{
|
||||
@ -76,7 +73,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
}
|
||||
}
|
||||
|
||||
public override (Vector2 position, double time) GetSnappedPosition(Vector2 position)
|
||||
public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double? fixedTime = null)
|
||||
{
|
||||
if (MaxIntervals == 0)
|
||||
return (StartPosition, StartTime);
|
||||
@ -100,8 +97,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (travelLength < DistanceBetweenTicks)
|
||||
travelLength = DistanceBetweenTicks;
|
||||
|
||||
float snappedDistance = LimitedDistanceSnap.Value
|
||||
? SnapProvider.DurationToDistance(ReferenceObject, editorClock.CurrentTime - ReferenceObject.GetEndTime())
|
||||
float snappedDistance = fixedTime != null
|
||||
? SnapProvider.DurationToDistance(ReferenceObject, fixedTime.Value - ReferenceObject.GetEndTime())
|
||||
// When interacting with the resolved snap provider, the distance spacing multiplier should first be removed
|
||||
// to allow for snapping at a non-multiplied ratio.
|
||||
: SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier, DistanceSnapTarget.End);
|
||||
|
@ -10,7 +10,6 @@ using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -61,18 +60,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
[Resolved]
|
||||
private BindableBeatDivisor beatDivisor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When enabled, distance snap should only snap to the current time (as per the editor clock).
|
||||
/// This is to emulate stable behaviour.
|
||||
/// </summary>
|
||||
protected Bindable<bool> LimitedDistanceSnap { get; private set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
LimitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
|
||||
}
|
||||
|
||||
private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);
|
||||
|
||||
protected readonly HitObject ReferenceObject;
|
||||
@ -143,8 +130,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// Snaps a position to this grid.
|
||||
/// </summary>
|
||||
/// <param name="position">The original position in coordinate space local to this <see cref="DistanceSnapGrid"/>.</param>
|
||||
/// <param name="fixedTime">
|
||||
/// Whether the snap operation should be temporally constrained to a particular time instant,
|
||||
/// thus fixing the possible positions to a set distance from the <see cref="ReferenceObject"/>.
|
||||
/// </param>
|
||||
/// <returns>A tuple containing the snapped position in coordinate space local to this <see cref="DistanceSnapGrid"/> and the respective time value.</returns>
|
||||
public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position);
|
||||
public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double? fixedTime = null);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the applicable colour for a beat index.
|
||||
|
@ -330,6 +330,18 @@ namespace osu.Game.Screens.Edit
|
||||
editorTimelineShowTicks = config.GetBindable<bool>(OsuSetting.EditorTimelineShowTicks);
|
||||
editorContractSidebars = config.GetBindable<bool>(OsuSetting.EditorContractSidebars);
|
||||
|
||||
// These two settings don't work together. Make them mutually exclusive to let the user know.
|
||||
editorAutoSeekOnPlacement.BindValueChanged(enabled =>
|
||||
{
|
||||
if (enabled.NewValue)
|
||||
editorLimitedDistanceSnap.Value = false;
|
||||
});
|
||||
editorLimitedDistanceSnap.BindValueChanged(enabled =>
|
||||
{
|
||||
if (enabled.NewValue)
|
||||
editorAutoSeekOnPlacement.Value = false;
|
||||
});
|
||||
|
||||
AddInternal(new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
@ -20,6 +20,8 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
public partial class ControlPointList : CompositeDrawable
|
||||
{
|
||||
public Action? SelectClosestTimingPoint { get; init; }
|
||||
|
||||
private ControlPointTable table = null!;
|
||||
private Container controls = null!;
|
||||
private OsuButton deleteButton = null!;
|
||||
@ -75,7 +77,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
new RoundedButton
|
||||
{
|
||||
Text = "Select closest to current time",
|
||||
Action = goToCurrentGroup,
|
||||
Action = SelectClosestTimingPoint,
|
||||
Size = new Vector2(220, 30),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
@ -146,17 +148,6 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
table.Padding = new MarginPadding { Bottom = controls.DrawHeight };
|
||||
}
|
||||
|
||||
private void goToCurrentGroup()
|
||||
{
|
||||
double accurateTime = clock.CurrentTimeAccurate;
|
||||
|
||||
var activeTimingPoint = Beatmap.ControlPointInfo.TimingPointAt(accurateTime);
|
||||
var activeEffectPoint = Beatmap.ControlPointInfo.EffectPointAt(accurateTime);
|
||||
|
||||
double latestActiveTime = Math.Max(activeTimingPoint.Time, activeEffectPoint.Time);
|
||||
selectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(latestActiveTime);
|
||||
}
|
||||
|
||||
private void delete()
|
||||
{
|
||||
if (selectedGroup.Value == null)
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -37,7 +38,10 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new ControlPointList(),
|
||||
new ControlPointList
|
||||
{
|
||||
SelectClosestTimingPoint = selectClosestTimingPoint,
|
||||
},
|
||||
new ControlPointSettings(),
|
||||
},
|
||||
}
|
||||
@ -70,8 +74,18 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
if (editorClock == null)
|
||||
return;
|
||||
|
||||
var nearestTimingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime);
|
||||
SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(nearestTimingPoint.Time);
|
||||
double accurateTime = editorClock.CurrentTimeAccurate;
|
||||
|
||||
var activeTimingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(accurateTime);
|
||||
var activeEffectPoint = EditorBeatmap.ControlPointInfo.EffectPointAt(accurateTime);
|
||||
|
||||
if (activeEffectPoint.Equals(EffectControlPoint.DEFAULT))
|
||||
SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(activeTimingPoint.Time);
|
||||
else
|
||||
{
|
||||
double latestActiveTime = Math.Max(activeTimingPoint.Time, activeEffectPoint.Time);
|
||||
SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(latestActiveTime);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ConfigureTimeline(TimelineArea timelineArea)
|
||||
|
@ -161,7 +161,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new DrawableRoomPlaylistItem(playlistItem)
|
||||
new DrawableRoomPlaylistItem(playlistItem, true)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AllowReordering = false,
|
||||
|
@ -142,10 +142,10 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
|
||||
request.Success += req => Schedule(() =>
|
||||
{
|
||||
var best = req.Scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo)).ToArray();
|
||||
var best = req.Scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, beatmap.Value.BeatmapInfo)).ToArray();
|
||||
|
||||
userBestScore.Value = req.UserScore;
|
||||
var userBest = userBestScore.Value?.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo);
|
||||
var userBest = userBestScore.Value?.CreateScoreInfo(scoreManager, rulesets, beatmap.Value.BeatmapInfo);
|
||||
|
||||
cancellationTokenSource?.Cancel();
|
||||
cancellationTokenSource = null;
|
||||
|
@ -74,7 +74,7 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
|
||||
public bool IsSelectedItem => SelectedItem.Value?.ID == Item.ID;
|
||||
|
||||
private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both };
|
||||
private readonly DelayedLoadWrapper onScreenLoader;
|
||||
private readonly IBindable<bool> valid = new Bindable<bool>();
|
||||
|
||||
private IBeatmapInfo? beatmap;
|
||||
@ -120,9 +120,11 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
[Resolved(CanBeNull = true)]
|
||||
private ManageCollectionsDialog? manageCollectionsDialog { get; set; }
|
||||
|
||||
public DrawableRoomPlaylistItem(PlaylistItem item)
|
||||
public DrawableRoomPlaylistItem(PlaylistItem item, bool loadImmediately = false)
|
||||
: base(item)
|
||||
{
|
||||
onScreenLoader = new DelayedLoadWrapper(Empty, timeBeforeLoad: loadImmediately ? 0 : 500) { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
Item = item;
|
||||
|
||||
valid.BindTo(item.Valid);
|
||||
|
@ -18,6 +18,7 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Select;
|
||||
using osuTK;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay
|
||||
{
|
||||
@ -36,8 +37,9 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
}
|
||||
}
|
||||
|
||||
private OsuSpriteText count = null!;
|
||||
public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); }
|
||||
|
||||
private OsuSpriteText count = null!;
|
||||
private Circle circle = null!;
|
||||
|
||||
private readonly FreeModSelectOverlay freeModSelectOverlay;
|
||||
@ -45,6 +47,9 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
public FooterButtonFreeMods(FreeModSelectOverlay freeModSelectOverlay)
|
||||
{
|
||||
this.freeModSelectOverlay = freeModSelectOverlay;
|
||||
|
||||
// Overwrite any external behaviour as we delegate the main toggle action to a sub-button.
|
||||
base.Action = toggleAllFreeMods;
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
@ -91,6 +96,8 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
SelectedColour = colours.Yellow;
|
||||
DeselectedColour = SelectedColour.Opacity(0.5f);
|
||||
Text = @"freemods";
|
||||
|
||||
TooltipText = MultiplayerMatchStrings.FreeModsButtonTooltip;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -98,9 +105,6 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
base.LoadComplete();
|
||||
|
||||
Current.BindValueChanged(_ => updateModDisplay(), true);
|
||||
|
||||
// Overwrite any external behaviour as we delegate the main toggle action to a sub-button.
|
||||
Action = toggleAllFreeMods;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
103
osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs
Normal file
103
osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs
Normal file
@ -0,0 +1,103 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay
|
||||
{
|
||||
public partial class FooterButtonFreestyle : FooterButton, IHasCurrentValue<bool>
|
||||
{
|
||||
private readonly BindableWithCurrent<bool> current = new BindableWithCurrent<bool>();
|
||||
|
||||
public Bindable<bool> Current
|
||||
{
|
||||
get => current.Current;
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); }
|
||||
|
||||
private OsuSpriteText text = null!;
|
||||
private Circle circle = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
public FooterButtonFreestyle()
|
||||
{
|
||||
// Overwrite any external behaviour as we delegate the main toggle action to a sub-button.
|
||||
base.Action = () => current.Value = !current.Value;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
ButtonContentContainer.AddRange(new[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
circle = new Circle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Colour = colours.YellowDark,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
text = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Padding = new MarginPadding(5),
|
||||
UseFullGlyphHeight = false,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
SelectedColour = colours.Yellow;
|
||||
DeselectedColour = SelectedColour.Opacity(0.5f);
|
||||
Text = @"freestyle";
|
||||
|
||||
TooltipText = MultiplayerMatchStrings.FreestyleButtonTooltip;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Current.BindValueChanged(_ => updateDisplay(), true);
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
if (current.Value)
|
||||
{
|
||||
text.Text = "on";
|
||||
text.FadeColour(colours.Gray2, 200, Easing.OutQuint);
|
||||
circle.FadeColour(colours.Yellow, 200, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
text.Text = "off";
|
||||
text.FadeColour(colours.GrayF, 200, Easing.OutQuint);
|
||||
circle.FadeColour(colours.Gray4, 200, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -179,6 +179,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft
|
||||
},
|
||||
new FreestyleStatusPill(Room)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft
|
||||
},
|
||||
endDateInfo = new EndDateInfo(Room)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
|
@ -0,0 +1,64 @@
|
||||
// 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.ComponentModel;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
{
|
||||
public partial class FreestyleStatusPill : OnlinePlayPill
|
||||
{
|
||||
private readonly Room room;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
protected override FontUsage Font => base.Font.With(weight: FontWeight.SemiBold);
|
||||
|
||||
public FreestyleStatusPill(Room room)
|
||||
{
|
||||
this.room = room;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Pill.Background.Alpha = 1;
|
||||
Pill.Background.Colour = colours.Yellow;
|
||||
|
||||
TextFlow.Text = "Freestyle";
|
||||
TextFlow.Colour = Color4.Black;
|
||||
|
||||
room.PropertyChanged += onRoomPropertyChanged;
|
||||
updateFreestyleStatus();
|
||||
}
|
||||
|
||||
private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(Room.CurrentPlaylistItem):
|
||||
case nameof(Room.Playlist):
|
||||
updateFreestyleStatus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFreestyleStatus()
|
||||
{
|
||||
PlaylistItem? currentItem = room.Playlist.GetCurrentItem() ?? room.CurrentPlaylistItem;
|
||||
Alpha = currentItem?.Freestyle == true ? 1 : 0;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
room.PropertyChanged -= onRoomPropertyChanged;
|
||||
}
|
||||
}
|
||||
}
|
@ -28,6 +28,7 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Utils;
|
||||
using Container = osu.Framework.Graphics.Containers.Container;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Match
|
||||
@ -50,7 +51,18 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
/// A container that provides controls for selection of user mods.
|
||||
/// This will be shown/hidden automatically when applicable.
|
||||
/// </summary>
|
||||
protected Drawable? UserModsSection;
|
||||
protected Drawable UserModsSection = null!;
|
||||
|
||||
/// <summary>
|
||||
/// A container that provides controls for selection of the user style.
|
||||
/// This will be shown/hidden automatically when applicable.
|
||||
/// </summary>
|
||||
protected Drawable UserStyleSection = null!;
|
||||
|
||||
/// <summary>
|
||||
/// A container that will display the user's style.
|
||||
/// </summary>
|
||||
protected Container<DrawableRoomPlaylistItem> UserStyleDisplayContainer = null!;
|
||||
|
||||
private Sample? sampleStart;
|
||||
|
||||
@ -254,11 +266,11 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged));
|
||||
UserMods.BindValueChanged(_ => Scheduler.AddOnce(UpdateMods));
|
||||
SelectedItem.BindValueChanged(_ => updateSpecifics());
|
||||
UserMods.BindValueChanged(_ => updateSpecifics());
|
||||
|
||||
beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem);
|
||||
beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateWorkingBeatmap());
|
||||
beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSpecifics());
|
||||
|
||||
userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(UserModsSelectOverlay);
|
||||
|
||||
@ -327,7 +339,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
public override void OnSuspending(ScreenTransitionEvent e)
|
||||
{
|
||||
// Should be a noop in most cases, but let's ensure beyond doubt that the beatmap is in a correct state.
|
||||
updateWorkingBeatmap();
|
||||
updateSpecifics();
|
||||
|
||||
onLeaving();
|
||||
base.OnSuspending(e);
|
||||
@ -336,10 +348,10 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
public override void OnResuming(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnResuming(e);
|
||||
updateWorkingBeatmap();
|
||||
|
||||
updateSpecifics();
|
||||
|
||||
beginHandlingTrack();
|
||||
Scheduler.AddOnce(UpdateMods);
|
||||
Scheduler.AddOnce(updateRuleset);
|
||||
}
|
||||
|
||||
protected bool ExitConfirmed { get; private set; }
|
||||
@ -389,9 +401,13 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
|
||||
protected void StartPlay()
|
||||
{
|
||||
if (SelectedItem.Value == null)
|
||||
if (SelectedItem.Value is not PlaylistItem item)
|
||||
return;
|
||||
|
||||
item = item.With(
|
||||
ruleset: GetGameplayRuleset().OnlineID,
|
||||
beatmap: new Optional<IBeatmapInfo>(GetGameplayBeatmap()));
|
||||
|
||||
// User may be at song select or otherwise when the host starts gameplay.
|
||||
// Ensure that they first return to this screen, else global bindables (beatmap etc.) may be in a bad state.
|
||||
if (!this.IsCurrentScreen())
|
||||
@ -407,7 +423,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
// fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes).
|
||||
var targetScreen = (Screen?)ParentScreen ?? this;
|
||||
|
||||
targetScreen.Push(CreateGameplayScreen(SelectedItem.Value));
|
||||
targetScreen.Push(CreateGameplayScreen(item));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -417,66 +433,75 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
/// <returns>The screen to enter.</returns>
|
||||
protected abstract Screen CreateGameplayScreen(PlaylistItem selectedItem);
|
||||
|
||||
private void selectedItemChanged()
|
||||
private void updateSpecifics()
|
||||
{
|
||||
updateWorkingBeatmap();
|
||||
|
||||
if (SelectedItem.Value is not PlaylistItem selected)
|
||||
if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item)
|
||||
return;
|
||||
|
||||
var rulesetInstance = Rulesets.GetRuleset(selected.RulesetID)?.CreateInstance();
|
||||
Debug.Assert(rulesetInstance != null);
|
||||
var allowedMods = selected.AllowedMods.Select(m => m.ToMod(rulesetInstance));
|
||||
var rulesetInstance = GetGameplayRuleset().CreateInstance();
|
||||
|
||||
// Remove any user mods that are no longer allowed.
|
||||
UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList();
|
||||
Mod[] allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray();
|
||||
if (!newUserMods.SequenceEqual(UserMods.Value))
|
||||
UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList();
|
||||
|
||||
UpdateMods();
|
||||
updateRuleset();
|
||||
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
|
||||
int beatmapId = GetGameplayBeatmap().OnlineID;
|
||||
var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == beatmapId);
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
|
||||
UserModsSelectOverlay.Beatmap.Value = Beatmap.Value;
|
||||
|
||||
if (!selected.AllowedMods.Any())
|
||||
Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
Ruleset.Value = GetGameplayRuleset();
|
||||
|
||||
bool freeMod = item.AllowedMods.Any();
|
||||
bool freestyle = item.Freestyle;
|
||||
|
||||
// For now, the game can never be in a state where freemod and freestyle are on at the same time.
|
||||
// This will change, but due to the current implementation if this was to occur drawables will overlap so let's assert.
|
||||
Debug.Assert(!freeMod || !freestyle);
|
||||
|
||||
if (freeMod)
|
||||
{
|
||||
UserModsSection?.Hide();
|
||||
UserModsSelectOverlay.Hide();
|
||||
UserModsSelectOverlay.IsValidMod = _ => false;
|
||||
UserModsSection.Show();
|
||||
UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType());
|
||||
}
|
||||
else
|
||||
{
|
||||
UserModsSection?.Show();
|
||||
UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType());
|
||||
UserModsSection.Hide();
|
||||
UserModsSelectOverlay.Hide();
|
||||
UserModsSelectOverlay.IsValidMod = _ => false;
|
||||
}
|
||||
|
||||
if (freestyle)
|
||||
{
|
||||
UserStyleSection.Show();
|
||||
|
||||
PlaylistItem gameplayItem = SelectedItem.Value.With(ruleset: GetGameplayRuleset().OnlineID, beatmap: new Optional<IBeatmapInfo>(GetGameplayBeatmap()));
|
||||
PlaylistItem? currentItem = UserStyleDisplayContainer.SingleOrDefault()?.Item;
|
||||
|
||||
if (gameplayItem.Equals(currentItem))
|
||||
return;
|
||||
|
||||
UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true)
|
||||
{
|
||||
AllowReordering = false,
|
||||
AllowEditing = freestyle,
|
||||
RequestEdit = _ => OpenStyleSelection()
|
||||
};
|
||||
}
|
||||
else
|
||||
UserStyleSection.Hide();
|
||||
}
|
||||
|
||||
private void updateWorkingBeatmap()
|
||||
{
|
||||
if (SelectedItem.Value == null || !this.IsCurrentScreen())
|
||||
return;
|
||||
protected virtual APIMod[] GetGameplayMods() => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray();
|
||||
|
||||
var beatmap = SelectedItem.Value?.Beatmap;
|
||||
protected virtual RulesetInfo GetGameplayRuleset() => Rulesets.GetRuleset(SelectedItem.Value!.RulesetID)!;
|
||||
|
||||
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
|
||||
var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID);
|
||||
protected virtual IBeatmapInfo GetGameplayBeatmap() => SelectedItem.Value!.Beatmap;
|
||||
|
||||
UserModsSelectOverlay.Beatmap.Value = Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
|
||||
}
|
||||
|
||||
protected virtual void UpdateMods()
|
||||
{
|
||||
if (SelectedItem.Value == null || !this.IsCurrentScreen())
|
||||
return;
|
||||
|
||||
var rulesetInstance = Rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance();
|
||||
Debug.Assert(rulesetInstance != null);
|
||||
Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList();
|
||||
}
|
||||
|
||||
private void updateRuleset()
|
||||
{
|
||||
if (SelectedItem.Value == null || !this.IsCurrentScreen())
|
||||
return;
|
||||
|
||||
Ruleset.Value = Rulesets.GetRuleset(SelectedItem.Value.RulesetID);
|
||||
}
|
||||
protected abstract void OpenStyleSelection();
|
||||
|
||||
private void beginHandlingTrack()
|
||||
{
|
||||
|
@ -0,0 +1,89 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
public partial class MultiplayerMatchFreestyleSelect : OnlinePlayFreestyleSelect
|
||||
{
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OngoingOperationTracker operationTracker { get; set; } = null!;
|
||||
|
||||
private readonly IBindable<bool> operationInProgress = new Bindable<bool>();
|
||||
|
||||
private LoadingLayer loadingLayer = null!;
|
||||
private IDisposable? selectionOperation;
|
||||
|
||||
public MultiplayerMatchFreestyleSelect(Room room, PlaylistItem item)
|
||||
: base(room, item)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(loadingLayer = new LoadingLayer(true));
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
operationInProgress.BindTo(operationTracker.InProgress);
|
||||
operationInProgress.BindValueChanged(_ => updateLoadingLayer(), true);
|
||||
}
|
||||
|
||||
private void updateLoadingLayer()
|
||||
{
|
||||
if (operationInProgress.Value)
|
||||
loadingLayer.Show();
|
||||
else
|
||||
loadingLayer.Hide();
|
||||
}
|
||||
|
||||
protected override bool OnStart()
|
||||
{
|
||||
if (operationInProgress.Value)
|
||||
{
|
||||
Logger.Log($"{nameof(OnStart)} aborted due to {nameof(operationInProgress)}");
|
||||
return false;
|
||||
}
|
||||
|
||||
selectionOperation = operationTracker.BeginOperation();
|
||||
|
||||
client.ChangeUserStyle(Beatmap.Value.BeatmapInfo.OnlineID, Ruleset.Value.OnlineID)
|
||||
.FireAndForget(onSuccess: () =>
|
||||
{
|
||||
selectionOperation.Dispose();
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
// If an error or server side trigger occurred this screen may have already exited by external means.
|
||||
if (this.IsCurrentScreen())
|
||||
this.Exit();
|
||||
});
|
||||
}, onError: _ =>
|
||||
{
|
||||
selectionOperation.Dispose();
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
Carousel.AllowSelection = true;
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -86,7 +86,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
BeatmapChecksum = item.Beatmap.MD5Hash,
|
||||
RulesetID = item.RulesetID,
|
||||
RequiredMods = item.RequiredMods.ToArray(),
|
||||
AllowedMods = item.AllowedMods.ToArray()
|
||||
AllowedMods = item.AllowedMods.ToArray(),
|
||||
Freestyle = item.Freestyle
|
||||
};
|
||||
|
||||
Task task = itemToEdit != null ? client.EditPlaylistItem(multiplayerItem) : client.AddPlaylistItem(multiplayerItem);
|
||||
|
@ -16,6 +16,8 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
@ -145,43 +147,66 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
SelectedItem = SelectedItem
|
||||
}
|
||||
},
|
||||
new[]
|
||||
new Drawable[]
|
||||
{
|
||||
UserModsSection = new FillFlowContainer
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Margin = new MarginPadding { Top = 10 },
|
||||
Alpha = 0,
|
||||
Children = new Drawable[]
|
||||
Children = new[]
|
||||
{
|
||||
new OverlinedHeader("Extra mods"),
|
||||
new FillFlowContainer
|
||||
UserModsSection = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(10, 0),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Alpha = 0,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new UserModSelectButton
|
||||
new OverlinedHeader("Extra mods"),
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Width = 90,
|
||||
Text = "Select",
|
||||
Action = ShowUserModSelect,
|
||||
},
|
||||
new ModDisplay
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Current = UserMods,
|
||||
Scale = new Vector2(0.8f),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(10, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new UserModSelectButton
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Width = 90,
|
||||
Text = "Select",
|
||||
Action = ShowUserModSelect,
|
||||
},
|
||||
new ModDisplay
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Current = UserMods,
|
||||
Scale = new Vector2(0.8f),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
UserStyleSection = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Alpha = 0,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OverlinedHeader("Difficulty"),
|
||||
UserStyleDisplayContainer = new Container<DrawableRoomPlaylistItem>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
RowDimensions = new[]
|
||||
@ -228,6 +253,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit));
|
||||
}
|
||||
|
||||
protected override void OpenStyleSelection()
|
||||
{
|
||||
if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item)
|
||||
return;
|
||||
|
||||
this.Push(new MultiplayerMatchFreestyleSelect(Room, item));
|
||||
}
|
||||
|
||||
protected override Drawable CreateFooter() => new MultiplayerMatchFooter
|
||||
{
|
||||
SelectedItem = SelectedItem
|
||||
@ -238,16 +271,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
SelectedItem = SelectedItem
|
||||
};
|
||||
|
||||
protected override void UpdateMods()
|
||||
protected override APIMod[] GetGameplayMods()
|
||||
{
|
||||
if (SelectedItem.Value == null || client.LocalUser == null || !this.IsCurrentScreen())
|
||||
return;
|
||||
// Using the room's reported status makes the server authoritative.
|
||||
return client.LocalUser?.Mods != null ? client.LocalUser.Mods.Concat(SelectedItem.Value!.RequiredMods).ToArray() : base.GetGameplayMods();
|
||||
}
|
||||
|
||||
// update local mods based on room's reported status for the local user (omitting the base call implementation).
|
||||
// this makes the server authoritative, and avoids the local user potentially setting mods that the server is not aware of (ie. if the match was started during the selection being changed).
|
||||
var rulesetInstance = Rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance();
|
||||
Debug.Assert(rulesetInstance != null);
|
||||
Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(rulesetInstance)).Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList();
|
||||
protected override RulesetInfo GetGameplayRuleset()
|
||||
{
|
||||
// Using the room's reported status makes the server authoritative.
|
||||
return client.LocalUser?.RulesetId != null ? Rulesets.GetRuleset(client.LocalUser.RulesetId.Value)! : base.GetGameplayRuleset();
|
||||
}
|
||||
|
||||
protected override IBeatmapInfo GetGameplayBeatmap()
|
||||
{
|
||||
// Using the room's reported status makes the server authoritative.
|
||||
return client.LocalUser?.BeatmapId != null ? new APIBeatmap { OnlineID = client.LocalUser.BeatmapId.Value } : base.GetGameplayBeatmap();
|
||||
}
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
@ -349,23 +388,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
return;
|
||||
}
|
||||
|
||||
updateCurrentItem();
|
||||
SelectedItem.Value = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId);
|
||||
|
||||
addItemButton.Alpha = localUserCanAddItem ? 1 : 0;
|
||||
|
||||
Scheduler.AddOnce(UpdateMods);
|
||||
|
||||
Activity.Value = new UserActivity.InLobby(Room);
|
||||
}
|
||||
|
||||
private bool localUserCanAddItem => client.IsHost || Room.QueueMode != QueueMode.HostOnly;
|
||||
|
||||
private void updateCurrentItem()
|
||||
{
|
||||
Debug.Assert(client.Room != null);
|
||||
SelectedItem.Value = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId);
|
||||
}
|
||||
|
||||
private void handleRoomLost() => Schedule(() =>
|
||||
{
|
||||
Logger.Log($"{this} exiting due to loss of room or connection");
|
||||
|
@ -4,6 +4,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
@ -14,6 +16,9 @@ using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -47,6 +52,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
private SpriteIcon crown = null!;
|
||||
|
||||
private OsuSpriteText userRankText = null!;
|
||||
private StyleDisplayIcon userStyleDisplay = null!;
|
||||
private ModDisplay userModsDisplay = null!;
|
||||
private StateDisplay userStateDisplay = null!;
|
||||
|
||||
@ -149,16 +155,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
}
|
||||
}
|
||||
},
|
||||
new Container
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding { Right = 70 },
|
||||
Child = userModsDisplay = new ModDisplay
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Scale = new Vector2(0.5f),
|
||||
ExpansionMode = ExpansionMode.AlwaysContracted,
|
||||
userStyleDisplay = new StyleDisplayIcon(),
|
||||
userModsDisplay = new ModDisplay
|
||||
{
|
||||
Scale = new Vector2(0.5f),
|
||||
ExpansionMode = ExpansionMode.AlwaysContracted,
|
||||
}
|
||||
}
|
||||
},
|
||||
userStateDisplay = new StateDisplay
|
||||
@ -208,9 +218,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability);
|
||||
|
||||
if ((User.BeatmapAvailability.State == DownloadState.LocallyAvailable) && (User.State != MultiplayerUserState.Spectating))
|
||||
{
|
||||
userModsDisplay.FadeIn(fade_time);
|
||||
userStyleDisplay.FadeIn(fade_time);
|
||||
}
|
||||
else
|
||||
{
|
||||
userModsDisplay.FadeOut(fade_time);
|
||||
userStyleDisplay.FadeOut(fade_time);
|
||||
}
|
||||
|
||||
if ((User.BeatmapId == null && User.RulesetId == null) || (User.BeatmapId == currentItem?.BeatmapID && User.RulesetId == currentItem?.RulesetID))
|
||||
userStyleDisplay.Style = null;
|
||||
else
|
||||
userStyleDisplay.Style = (User.BeatmapId ?? currentItem?.BeatmapID ?? 0, User.RulesetId ?? currentItem?.RulesetID ?? 0);
|
||||
|
||||
kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0;
|
||||
crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0;
|
||||
@ -284,5 +305,81 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
IconHoverColour = colours.Red;
|
||||
}
|
||||
}
|
||||
|
||||
private partial class StyleDisplayIcon : CompositeComponent
|
||||
{
|
||||
[Resolved]
|
||||
private BeatmapLookupCache beatmapLookupCache { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
public StyleDisplayIcon()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
private (int beatmap, int ruleset)? style;
|
||||
|
||||
public (int beatmap, int ruleset)? Style
|
||||
{
|
||||
get => style;
|
||||
set
|
||||
{
|
||||
if (style == value)
|
||||
return;
|
||||
|
||||
style = value;
|
||||
Scheduler.Add(refresh);
|
||||
}
|
||||
}
|
||||
|
||||
private CancellationTokenSource? cancellationSource;
|
||||
|
||||
private void refresh()
|
||||
{
|
||||
cancellationSource?.Cancel();
|
||||
cancellationSource?.Dispose();
|
||||
cancellationSource = null;
|
||||
|
||||
if (Style == null)
|
||||
{
|
||||
ClearInternal();
|
||||
return;
|
||||
}
|
||||
|
||||
cancellationSource = new CancellationTokenSource();
|
||||
CancellationToken token = cancellationSource.Token;
|
||||
|
||||
int localBeatmap = Style.Value.beatmap;
|
||||
int localRuleset = Style.Value.ruleset;
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var beatmap = await beatmapLookupCache.GetBeatmapAsync(localBeatmap, token).ConfigureAwait(false);
|
||||
if (beatmap == null)
|
||||
return;
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
InternalChild = new DifficultyIcon(beatmap, rulesets.GetRuleset(localRuleset))
|
||||
{
|
||||
Size = new Vector2(20),
|
||||
TooltipType = DifficultyIconTooltipType.Extended,
|
||||
};
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log($"Error while populating participant style icon {e}");
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
104
osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs
Normal file
104
osu.Game/Screens/OnlinePlay/OnlinePlayFreestyleSelect.cs
Normal file
@ -0,0 +1,104 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Humanizer;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay
|
||||
{
|
||||
public abstract partial class OnlinePlayFreestyleSelect : SongSelect, IOnlinePlaySubScreen, IHandlePresentBeatmap
|
||||
{
|
||||
public string ShortTitle => "style selection";
|
||||
|
||||
public override string Title => ShortTitle.Humanize();
|
||||
|
||||
public override bool AllowEditing => false;
|
||||
|
||||
protected override UserActivity InitialActivity => new UserActivity.InLobby(room);
|
||||
|
||||
private readonly Room room;
|
||||
private readonly PlaylistItem item;
|
||||
|
||||
protected OnlinePlayFreestyleSelect(Room room, PlaylistItem item)
|
||||
{
|
||||
this.room = room;
|
||||
this.item = item;
|
||||
|
||||
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT };
|
||||
}
|
||||
|
||||
protected override FilterControl CreateFilterControl() => new DifficultySelectFilterControl(item);
|
||||
|
||||
protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons()
|
||||
{
|
||||
// Required to create the drawable components.
|
||||
base.CreateSongSelectFooterButtons();
|
||||
return Enumerable.Empty<(FooterButton, OverlayContainer?)>();
|
||||
}
|
||||
|
||||
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
|
||||
|
||||
public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset)
|
||||
{
|
||||
// This screen cannot present beatmaps.
|
||||
}
|
||||
|
||||
private partial class DifficultySelectFilterControl : FilterControl
|
||||
{
|
||||
private readonly PlaylistItem item;
|
||||
private double itemLength;
|
||||
private int beatmapSetId;
|
||||
|
||||
public DifficultySelectFilterControl(PlaylistItem item)
|
||||
{
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RealmAccess realm)
|
||||
{
|
||||
realm.Run(r =>
|
||||
{
|
||||
int beatmapId = item.Beatmap.OnlineID;
|
||||
BeatmapInfo? beatmap = r.All<BeatmapInfo>().FirstOrDefault(b => b.OnlineID == beatmapId);
|
||||
|
||||
itemLength = beatmap?.Length ?? 0;
|
||||
beatmapSetId = beatmap?.BeatmapSet?.OnlineID ?? 0;
|
||||
});
|
||||
}
|
||||
|
||||
public override FilterCriteria CreateCriteria()
|
||||
{
|
||||
var criteria = base.CreateCriteria();
|
||||
|
||||
// Must be from the same set as the playlist item.
|
||||
criteria.BeatmapSetId = beatmapSetId;
|
||||
criteria.HasOnlineID = true;
|
||||
|
||||
// Must be within 30s of the playlist item.
|
||||
criteria.Length.Min = itemLength - 30000;
|
||||
criteria.Length.Max = itemLength + 30000;
|
||||
criteria.Length.IsLowerInclusive = true;
|
||||
criteria.Length.IsUpperInclusive = true;
|
||||
|
||||
return criteria;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Utils;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay
|
||||
{
|
||||
@ -41,10 +42,12 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
protected override UserActivity InitialActivity => new UserActivity.InLobby(room);
|
||||
|
||||
protected readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
||||
protected readonly Bindable<bool> Freestyle = new Bindable<bool>();
|
||||
|
||||
private readonly Room room;
|
||||
private readonly PlaylistItem? initialItem;
|
||||
private readonly FreeModSelectOverlay freeModSelectOverlay;
|
||||
private readonly FreeModSelectOverlay freeModSelect;
|
||||
private FooterButton freeModsFooterButton = null!;
|
||||
|
||||
private IDisposable? freeModSelectOverlayRegistration;
|
||||
|
||||
@ -61,7 +64,7 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
|
||||
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
|
||||
|
||||
freeModSelectOverlay = new FreeModSelectOverlay
|
||||
freeModSelect = new FreeModSelectOverlay
|
||||
{
|
||||
SelectedMods = { BindTarget = FreeMods },
|
||||
IsValidMod = IsValidFreeMod,
|
||||
@ -72,7 +75,7 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
private void load()
|
||||
{
|
||||
LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT };
|
||||
LoadComponent(freeModSelectOverlay);
|
||||
LoadComponent(freeModSelect);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -108,12 +111,35 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
Mods.Value = initialItem.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
FreeMods.Value = initialItem.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
}
|
||||
|
||||
Freestyle.Value = initialItem.Freestyle;
|
||||
}
|
||||
|
||||
Mods.BindValueChanged(onModsChanged);
|
||||
Ruleset.BindValueChanged(onRulesetChanged);
|
||||
Freestyle.BindValueChanged(onFreestyleChanged, true);
|
||||
|
||||
freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelectOverlay);
|
||||
freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelect);
|
||||
}
|
||||
|
||||
private void onFreestyleChanged(ValueChangedEvent<bool> enabled)
|
||||
{
|
||||
if (enabled.NewValue)
|
||||
{
|
||||
freeModsFooterButton.Enabled.Value = false;
|
||||
ModsFooterButton.Enabled.Value = false;
|
||||
|
||||
ModSelect.Hide();
|
||||
freeModSelect.Hide();
|
||||
|
||||
Mods.Value = [];
|
||||
FreeMods.Value = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
freeModsFooterButton.Enabled.Value = true;
|
||||
ModsFooterButton.Enabled.Value = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void onModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
|
||||
@ -121,7 +147,7 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
FreeMods.Value = FreeMods.Value.Where(checkCompatibleFreeMod).ToList();
|
||||
|
||||
// Reset the validity delegate to update the overlay's display.
|
||||
freeModSelectOverlay.IsValidMod = IsValidFreeMod;
|
||||
freeModSelect.IsValidMod = IsValidFreeMod;
|
||||
}
|
||||
|
||||
private void onRulesetChanged(ValueChangedEvent<RulesetInfo> ruleset)
|
||||
@ -135,7 +161,8 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
{
|
||||
RulesetID = Ruleset.Value.OnlineID,
|
||||
RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(),
|
||||
AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray()
|
||||
AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(),
|
||||
Freestyle = Freestyle.Value
|
||||
};
|
||||
|
||||
return SelectItem(item);
|
||||
@ -150,9 +177,9 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
|
||||
public override bool OnBackButton()
|
||||
{
|
||||
if (freeModSelectOverlay.State.Value == Visibility.Visible)
|
||||
if (freeModSelect.State.Value == Visibility.Visible)
|
||||
{
|
||||
freeModSelectOverlay.Hide();
|
||||
freeModSelect.Hide();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -161,7 +188,7 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
|
||||
public override bool OnExiting(ScreenExitEvent e)
|
||||
{
|
||||
freeModSelectOverlay.Hide();
|
||||
freeModSelect.Hide();
|
||||
return base.OnExiting(e);
|
||||
}
|
||||
|
||||
@ -170,12 +197,17 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
IsValidMod = IsValidMod
|
||||
};
|
||||
|
||||
protected override IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons()
|
||||
protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons()
|
||||
{
|
||||
var baseButtons = base.CreateSongSelectFooterButtons().ToList();
|
||||
var freeModsButton = new FooterButtonFreeMods(freeModSelectOverlay) { Current = FreeMods };
|
||||
|
||||
baseButtons.Insert(baseButtons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (freeModsButton, freeModSelectOverlay));
|
||||
baseButtons.Single(i => i.button is FooterButtonMods).button.TooltipText = MultiplayerMatchStrings.RequiredModsButtonTooltip;
|
||||
|
||||
baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[]
|
||||
{
|
||||
(freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }, null),
|
||||
(new FooterButtonFreestyle { Current = Freestyle }, null)
|
||||
});
|
||||
|
||||
return baseButtons;
|
||||
}
|
||||
|
@ -189,7 +189,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
/// <param name="pivot">An optional pivot around which the scores were retrieved.</param>
|
||||
protected virtual ScoreInfo[] PerformSuccessCallback(Action<IEnumerable<ScoreInfo>> callback, List<MultiplayerScore> scores, MultiplayerScores? pivot = null)
|
||||
{
|
||||
var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray();
|
||||
var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray();
|
||||
|
||||
// Invoke callback to add the scores. Exclude the score provided to this screen since it's added already.
|
||||
callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID));
|
||||
|
@ -0,0 +1,37 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
{
|
||||
public partial class PlaylistsRoomFreestyleSelect : OnlinePlayFreestyleSelect
|
||||
{
|
||||
public new readonly Bindable<BeatmapInfo?> Beatmap = new Bindable<BeatmapInfo?>();
|
||||
public new readonly Bindable<RulesetInfo?> Ruleset = new Bindable<RulesetInfo?>();
|
||||
|
||||
public PlaylistsRoomFreestyleSelect(Room room, PlaylistItem item)
|
||||
: base(room, item)
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool OnStart()
|
||||
{
|
||||
// Beatmaps without a valid online ID are filtered away; this is just a final safety.
|
||||
if (base.Beatmap.Value.BeatmapInfo.OnlineID < 0)
|
||||
return false;
|
||||
|
||||
if (base.Ruleset.Value.OnlineID < 0)
|
||||
return false;
|
||||
|
||||
Beatmap.Value = base.Beatmap.Value.BeatmapInfo;
|
||||
Ruleset.Value = base.Ruleset.Value;
|
||||
this.Exit();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -11,11 +11,13 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Input;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Match;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
@ -46,6 +48,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
private FillFlowContainer progressSection = null!;
|
||||
private DrawableRoomPlaylist drawablePlaylist = null!;
|
||||
|
||||
private readonly Bindable<BeatmapInfo?> userBeatmap = new Bindable<BeatmapInfo?>();
|
||||
private readonly Bindable<RulesetInfo?> userRuleset = new Bindable<RulesetInfo?>();
|
||||
|
||||
public PlaylistsRoomSubScreen(Room room)
|
||||
: base(room, false) // Editing is temporarily not allowed.
|
||||
{
|
||||
@ -66,6 +71,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
SelectedItem.BindValueChanged(onSelectedItemChanged, true);
|
||||
isIdle.BindValueChanged(_ => updatePollingRate(), true);
|
||||
|
||||
Room.PropertyChanged += onRoomPropertyChanged;
|
||||
@ -74,6 +80,16 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
updateRoomPlaylist();
|
||||
}
|
||||
|
||||
private void onSelectedItemChanged(ValueChangedEvent<PlaylistItem?> item)
|
||||
{
|
||||
// Simplest for now.
|
||||
userBeatmap.Value = null;
|
||||
userRuleset.Value = null;
|
||||
}
|
||||
|
||||
protected override IBeatmapInfo GetGameplayBeatmap() => userBeatmap.Value ?? base.GetGameplayBeatmap();
|
||||
protected override RulesetInfo GetGameplayRuleset() => userRuleset.Value ?? base.GetGameplayRuleset();
|
||||
|
||||
private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
@ -168,41 +184,65 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
new Drawable[]
|
||||
{
|
||||
UserModsSection = new FillFlowContainer
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Alpha = 0,
|
||||
Margin = new MarginPadding { Bottom = 10 },
|
||||
Children = new Drawable[]
|
||||
Children = new[]
|
||||
{
|
||||
new OverlinedHeader("Extra mods"),
|
||||
new FillFlowContainer
|
||||
UserModsSection = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(10, 0),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Alpha = 0,
|
||||
Margin = new MarginPadding { Bottom = 10 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new UserModSelectButton
|
||||
new OverlinedHeader("Extra mods"),
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Width = 90,
|
||||
Text = "Select",
|
||||
Action = ShowUserModSelect,
|
||||
},
|
||||
new ModDisplay
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Current = UserMods,
|
||||
Scale = new Vector2(0.8f),
|
||||
},
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(10, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new UserModSelectButton
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Width = 90,
|
||||
Text = "Select",
|
||||
Action = ShowUserModSelect,
|
||||
},
|
||||
new ModDisplay
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Current = UserMods,
|
||||
Scale = new Vector2(0.8f),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
UserStyleSection = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Alpha = 0,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OverlinedHeader("Difficulty"),
|
||||
UserStyleDisplayContainer = new Container<DrawableRoomPlaylistItem>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -274,6 +314,18 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
},
|
||||
};
|
||||
|
||||
protected override void OpenStyleSelection()
|
||||
{
|
||||
if (!this.IsCurrentScreen() || SelectedItem.Value is not PlaylistItem item)
|
||||
return;
|
||||
|
||||
this.Push(new PlaylistsRoomFreestyleSelect(Room, item)
|
||||
{
|
||||
Beatmap = { BindTarget = userBeatmap },
|
||||
Ruleset = { BindTarget = userRuleset }
|
||||
});
|
||||
}
|
||||
|
||||
private void updatePollingRate()
|
||||
{
|
||||
selectionPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 30000 : 5000;
|
||||
|
@ -39,7 +39,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
ID = room.Playlist.Count == 0 ? 0 : room.Playlist.Max(p => p.ID) + 1,
|
||||
RulesetID = Ruleset.Value.OnlineID,
|
||||
RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(),
|
||||
AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray()
|
||||
AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray(),
|
||||
Freestyle = Freestyle.Value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Storyboards;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
@ -53,7 +54,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private readonly Bindable<bool> playbackRateValid = new Bindable<bool>(true);
|
||||
|
||||
private readonly WorkingBeatmap beatmap;
|
||||
private readonly IBeatmap beatmap;
|
||||
|
||||
private Track track;
|
||||
|
||||
@ -63,20 +64,19 @@ namespace osu.Game.Screens.Play
|
||||
/// <summary>
|
||||
/// Create a new master gameplay clock container.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap to be used for time and metadata references.</param>
|
||||
/// <param name="working">The beatmap to be used for time and metadata references.</param>
|
||||
/// <param name="gameplayStartTime">The latest time which should be used when introducing gameplay. Will be used when skipping forward.</param>
|
||||
public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime)
|
||||
: base(beatmap.Track, applyOffsets: true, requireDecoupling: true)
|
||||
public MasterGameplayClockContainer(WorkingBeatmap working, double gameplayStartTime)
|
||||
: base(working.Track, applyOffsets: true, requireDecoupling: true)
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
|
||||
track = beatmap.Track;
|
||||
beatmap = working.Beatmap;
|
||||
track = working.Track;
|
||||
|
||||
GameplayStartTime = gameplayStartTime;
|
||||
StartTime = findEarliestStartTime(gameplayStartTime, beatmap);
|
||||
StartTime = findEarliestStartTime(gameplayStartTime, beatmap, working.Storyboard);
|
||||
}
|
||||
|
||||
private static double findEarliestStartTime(double gameplayStartTime, WorkingBeatmap beatmap)
|
||||
private static double findEarliestStartTime(double gameplayStartTime, IBeatmap beatmap, Storyboard storyboard)
|
||||
{
|
||||
// here we are trying to find the time to start playback from the "zero" point.
|
||||
// generally this is either zero, or some point earlier than zero in the case of storyboards, lead-ins etc.
|
||||
@ -86,15 +86,15 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
// if a storyboard is present, it may dictate the appropriate start time by having events in negative time space.
|
||||
// this is commonly used to display an intro before the audio track start.
|
||||
double? firstStoryboardEvent = beatmap.Storyboard.EarliestEventTime;
|
||||
double? firstStoryboardEvent = storyboard.EarliestEventTime;
|
||||
if (firstStoryboardEvent != null)
|
||||
time = Math.Min(time, firstStoryboardEvent.Value);
|
||||
|
||||
// some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available.
|
||||
// this is not available as an option in the live editor but can still be applied via .osu editing.
|
||||
double firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime;
|
||||
if (beatmap.Beatmap.AudioLeadIn > 0)
|
||||
time = Math.Min(time, firstHitObjectTime - beatmap.Beatmap.AudioLeadIn);
|
||||
double firstHitObjectTime = beatmap.HitObjects.First().StartTime;
|
||||
if (beatmap.AudioLeadIn > 0)
|
||||
time = Math.Min(time, firstHitObjectTime - beatmap.AudioLeadIn);
|
||||
|
||||
return time;
|
||||
}
|
||||
@ -136,7 +136,7 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
removeAdjustmentsFromTrack();
|
||||
|
||||
track = new TrackVirtual(beatmap.Track.Length);
|
||||
track = new TrackVirtual(track.Length);
|
||||
track.Seek(CurrentTime);
|
||||
if (IsRunning)
|
||||
track.Start();
|
||||
@ -228,9 +228,8 @@ namespace osu.Game.Screens.Play
|
||||
removeAdjustmentsFromTrack();
|
||||
}
|
||||
|
||||
ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.Beatmap.ControlPointInfo;
|
||||
ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.ControlPointInfo;
|
||||
ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => track.CurrentAmplitudes;
|
||||
IClock IBeatSyncProvider.Clock => this;
|
||||
|
||||
ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => beatmap.TrackLoaded ? beatmap.Track.CurrentAmplitudes : ChannelAmplitudes.Empty;
|
||||
}
|
||||
}
|
||||
|
@ -90,6 +90,12 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
if (match && criteria.RulesetCriteria != null)
|
||||
match &= criteria.RulesetCriteria.Matches(BeatmapInfo, criteria);
|
||||
|
||||
if (match && criteria.HasOnlineID == true)
|
||||
match &= BeatmapInfo.OnlineID >= 0;
|
||||
|
||||
if (match && criteria.BeatmapSetId != null)
|
||||
match &= criteria.BeatmapSetId == BeatmapInfo.BeatmapSet?.OnlineID;
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
|
@ -57,7 +57,7 @@ namespace osu.Game.Screens.Select
|
||||
[CanBeNull]
|
||||
private FilterCriteria currentCriteria;
|
||||
|
||||
public FilterCriteria CreateCriteria()
|
||||
public virtual FilterCriteria CreateCriteria()
|
||||
{
|
||||
string query = searchTextBox.Text;
|
||||
|
||||
|
@ -56,6 +56,9 @@ namespace osu.Game.Screens.Select
|
||||
public RulesetInfo? Ruleset;
|
||||
public IReadOnlyList<Mod>? Mods;
|
||||
public bool AllowConvertedBeatmaps;
|
||||
public int? BeatmapSetId;
|
||||
|
||||
public bool? HasOnlineID;
|
||||
|
||||
private string searchText = string.Empty;
|
||||
|
||||
|
@ -83,6 +83,11 @@ namespace osu.Game.Screens.Select
|
||||
/// </summary>
|
||||
protected Container FooterPanels { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="FooterButton"/> that opens the mod select dialog.
|
||||
/// </summary>
|
||||
protected FooterButton ModsFooterButton { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Whether entering editor mode should be allowed.
|
||||
/// </summary>
|
||||
@ -214,11 +219,11 @@ namespace osu.Game.Screens.Select
|
||||
},
|
||||
}
|
||||
},
|
||||
FilterControl = new FilterControl
|
||||
FilterControl = CreateFilterControl().With(d =>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = FilterControl.HEIGHT,
|
||||
},
|
||||
d.RelativeSizeAxes = Axes.X;
|
||||
d.Height = FilterControl.HEIGHT;
|
||||
}),
|
||||
new GridContainer // used for max width implementation
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -387,6 +392,8 @@ namespace osu.Game.Screens.Select
|
||||
SampleConfirm = audio.Samples.Get(@"SongSelect/confirm-selection");
|
||||
}
|
||||
|
||||
protected virtual FilterControl CreateFilterControl() => new FilterControl();
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
@ -408,9 +415,9 @@ namespace osu.Game.Screens.Select
|
||||
/// Creates the buttons to be displayed in the footer.
|
||||
/// </summary>
|
||||
/// <returns>A set of <see cref="FooterButton"/> and an optional <see cref="OverlayContainer"/> which the button opens when pressed.</returns>
|
||||
protected virtual IEnumerable<(FooterButton, OverlayContainer?)> CreateSongSelectFooterButtons() => new (FooterButton, OverlayContainer?)[]
|
||||
protected virtual IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons() => new (FooterButton, OverlayContainer?)[]
|
||||
{
|
||||
(new FooterButtonMods { Current = Mods }, ModSelect),
|
||||
(ModsFooterButton = new FooterButtonMods { Current = Mods }, ModSelect),
|
||||
(new FooterButtonRandom
|
||||
{
|
||||
NextRandom = () => Carousel.SelectNextRandom(),
|
||||
|
@ -92,35 +92,113 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
#region Selection handling
|
||||
|
||||
protected override void HandleItemSelected(object? model)
|
||||
private GroupDefinition? lastSelectedGroup;
|
||||
private BeatmapInfo? lastSelectedBeatmap;
|
||||
|
||||
protected override bool HandleItemSelected(object? model)
|
||||
{
|
||||
base.HandleItemSelected(model);
|
||||
|
||||
// Selecting a set isn't valid – let's re-select the first difficulty.
|
||||
if (model is BeatmapSetInfo setInfo)
|
||||
switch (model)
|
||||
{
|
||||
CurrentSelection = setInfo.Beatmaps.First();
|
||||
return;
|
||||
case GroupDefinition group:
|
||||
// Special case – collapsing an open group.
|
||||
if (lastSelectedGroup == group)
|
||||
{
|
||||
setExpansionStateOfGroup(lastSelectedGroup, false);
|
||||
lastSelectedGroup = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
setExpandedGroup(group);
|
||||
return false;
|
||||
|
||||
case BeatmapSetInfo setInfo:
|
||||
// Selecting a set isn't valid – let's re-select the first difficulty.
|
||||
CurrentSelection = setInfo.Beatmaps.First();
|
||||
return false;
|
||||
|
||||
case BeatmapInfo beatmapInfo:
|
||||
|
||||
// If we have groups, we need to account for them.
|
||||
if (Criteria.SplitOutDifficulties)
|
||||
{
|
||||
// Find the containing group. There should never be too many groups so iterating is efficient enough.
|
||||
GroupDefinition? group = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key;
|
||||
|
||||
if (group != null)
|
||||
setExpandedGroup(group);
|
||||
}
|
||||
else
|
||||
{
|
||||
setExpandedSet(beatmapInfo);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (model is BeatmapInfo beatmapInfo)
|
||||
setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void HandleItemDeselected(object? model)
|
||||
protected override bool CheckValidForGroupSelection(CarouselItem item)
|
||||
{
|
||||
base.HandleItemDeselected(model);
|
||||
|
||||
if (model is BeatmapInfo beatmapInfo)
|
||||
setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, false);
|
||||
}
|
||||
|
||||
private void setVisibilityOfSetItems(BeatmapSetInfo set, bool visible)
|
||||
{
|
||||
if (grouping.SetItems.TryGetValue(set, out var group))
|
||||
switch (item.Model)
|
||||
{
|
||||
foreach (var i in group)
|
||||
i.IsVisible = visible;
|
||||
case BeatmapSetInfo:
|
||||
return true;
|
||||
|
||||
case BeatmapInfo:
|
||||
return Criteria.SplitOutDifficulties;
|
||||
|
||||
case GroupDefinition:
|
||||
return false;
|
||||
|
||||
default:
|
||||
throw new ArgumentException($"Unsupported model type {item.Model}");
|
||||
}
|
||||
}
|
||||
|
||||
private void setExpandedGroup(GroupDefinition group)
|
||||
{
|
||||
if (lastSelectedGroup != null)
|
||||
setExpansionStateOfGroup(lastSelectedGroup, false);
|
||||
lastSelectedGroup = group;
|
||||
setExpansionStateOfGroup(group, true);
|
||||
}
|
||||
|
||||
private void setExpansionStateOfGroup(GroupDefinition group, bool expanded)
|
||||
{
|
||||
if (grouping.GroupItems.TryGetValue(group, out var items))
|
||||
{
|
||||
foreach (var i in items)
|
||||
{
|
||||
if (i.Model is GroupDefinition)
|
||||
i.IsExpanded = expanded;
|
||||
else
|
||||
i.IsVisible = expanded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setExpandedSet(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
if (lastSelectedBeatmap != null)
|
||||
setExpansionStateOfSetItems(lastSelectedBeatmap.BeatmapSet!, false);
|
||||
lastSelectedBeatmap = beatmapInfo;
|
||||
setExpansionStateOfSetItems(beatmapInfo.BeatmapSet!, true);
|
||||
}
|
||||
|
||||
private void setExpansionStateOfSetItems(BeatmapSetInfo set, bool expanded)
|
||||
{
|
||||
if (grouping.SetItems.TryGetValue(set, out var items))
|
||||
{
|
||||
foreach (var i in items)
|
||||
{
|
||||
if (i.Model is BeatmapSetInfo)
|
||||
i.IsExpanded = expanded;
|
||||
else
|
||||
i.IsVisible = expanded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,9 +221,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
private readonly DrawablePool<BeatmapPanel> beatmapPanelPool = new DrawablePool<BeatmapPanel>(100);
|
||||
private readonly DrawablePool<BeatmapSetPanel> setPanelPool = new DrawablePool<BeatmapSetPanel>(100);
|
||||
private readonly DrawablePool<GroupPanel> groupPanelPool = new DrawablePool<GroupPanel>(100);
|
||||
|
||||
private void setupPools()
|
||||
{
|
||||
AddInternal(groupPanelPool);
|
||||
AddInternal(beatmapPanelPool);
|
||||
AddInternal(setPanelPool);
|
||||
}
|
||||
@ -154,7 +234,12 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
switch (item.Model)
|
||||
{
|
||||
case GroupDefinition:
|
||||
return groupPanelPool.Get();
|
||||
|
||||
case BeatmapInfo:
|
||||
// TODO: if beatmap is a group selection target, it needs to be a different drawable
|
||||
// with more information attached.
|
||||
return beatmapPanelPool.Get();
|
||||
|
||||
case BeatmapSetInfo:
|
||||
@ -166,4 +251,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public record GroupDefinition(string Title);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
@ -18,7 +19,13 @@ namespace osu.Game.Screens.SelectV2
|
||||
/// </summary>
|
||||
public IDictionary<BeatmapSetInfo, HashSet<CarouselItem>> SetItems => setItems;
|
||||
|
||||
/// <summary>
|
||||
/// Groups contain children which are group-selectable. This dictionary holds the relationships between groups-panels to allow expanding them on selection.
|
||||
/// </summary>
|
||||
public IDictionary<GroupDefinition, HashSet<CarouselItem>> GroupItems => groupItems;
|
||||
|
||||
private readonly Dictionary<BeatmapSetInfo, HashSet<CarouselItem>> setItems = new Dictionary<BeatmapSetInfo, HashSet<CarouselItem>>();
|
||||
private readonly Dictionary<GroupDefinition, HashSet<CarouselItem>> groupItems = new Dictionary<GroupDefinition, HashSet<CarouselItem>>();
|
||||
|
||||
private readonly Func<FilterCriteria> getCriteria;
|
||||
|
||||
@ -29,50 +36,97 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
public async Task<IEnumerable<CarouselItem>> Run(IEnumerable<CarouselItem> items, CancellationToken cancellationToken) => await Task.Run(() =>
|
||||
{
|
||||
bool groupSetsTogether;
|
||||
|
||||
setItems.Clear();
|
||||
groupItems.Clear();
|
||||
|
||||
var criteria = getCriteria();
|
||||
|
||||
if (criteria.SplitOutDifficulties)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
item.IsVisible = true;
|
||||
item.IsGroupSelectionTarget = true;
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
CarouselItem? lastItem = null;
|
||||
|
||||
var newItems = new List<CarouselItem>(items.Count());
|
||||
|
||||
foreach (var item in items)
|
||||
// Add criteria groups.
|
||||
switch (criteria.Group)
|
||||
{
|
||||
default:
|
||||
groupSetsTogether = true;
|
||||
newItems.AddRange(items);
|
||||
break;
|
||||
|
||||
case GroupMode.Difficulty:
|
||||
groupSetsTogether = false;
|
||||
int starGroup = int.MinValue;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var b = (BeatmapInfo)item.Model;
|
||||
|
||||
if (b.StarRating > starGroup)
|
||||
{
|
||||
starGroup = (int)Math.Floor(b.StarRating);
|
||||
var groupDefinition = new GroupDefinition($"{starGroup} - {++starGroup} *");
|
||||
var groupItem = new CarouselItem(groupDefinition) { DrawHeight = GroupPanel.HEIGHT };
|
||||
|
||||
newItems.Add(groupItem);
|
||||
groupItems[groupDefinition] = new HashSet<CarouselItem> { groupItem };
|
||||
}
|
||||
|
||||
newItems.Add(item);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Add set headers wherever required.
|
||||
CarouselItem? lastItem = null;
|
||||
|
||||
if (groupSetsTogether)
|
||||
{
|
||||
for (int i = 0; i < newItems.Count; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var item = newItems[i];
|
||||
|
||||
if (item.Model is BeatmapInfo beatmap)
|
||||
{
|
||||
bool newBeatmapSet = lastItem == null || (lastItem.Model is BeatmapInfo lastBeatmap && lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID);
|
||||
|
||||
if (newBeatmapSet)
|
||||
{
|
||||
var setItem = new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT };
|
||||
setItems[beatmap.BeatmapSet!] = new HashSet<CarouselItem> { setItem };
|
||||
newItems.Insert(i, setItem);
|
||||
i++;
|
||||
}
|
||||
|
||||
setItems[beatmap.BeatmapSet!].Add(item);
|
||||
item.IsVisible = false;
|
||||
}
|
||||
|
||||
lastItem = item;
|
||||
}
|
||||
}
|
||||
|
||||
// Link group items to their headers.
|
||||
GroupDefinition? lastGroup = null;
|
||||
|
||||
foreach (var item in newItems)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (item.Model is BeatmapInfo b)
|
||||
if (item.Model is GroupDefinition group)
|
||||
{
|
||||
// Add set header
|
||||
if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b.BeatmapSet!.OnlineID))
|
||||
{
|
||||
newItems.Add(new CarouselItem(b.BeatmapSet!)
|
||||
{
|
||||
DrawHeight = BeatmapSetPanel.HEIGHT,
|
||||
IsGroupSelectionTarget = true
|
||||
});
|
||||
}
|
||||
|
||||
if (!setItems.TryGetValue(b.BeatmapSet!, out var related))
|
||||
setItems[b.BeatmapSet!] = related = new HashSet<CarouselItem>();
|
||||
|
||||
related.Add(item);
|
||||
lastGroup = group;
|
||||
continue;
|
||||
}
|
||||
|
||||
newItems.Add(item);
|
||||
lastItem = item;
|
||||
|
||||
item.IsGroupSelectionTarget = false;
|
||||
item.IsVisible = false;
|
||||
if (lastGroup != null)
|
||||
{
|
||||
groupItems[lastGroup].Add(item);
|
||||
item.IsVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
return newItems;
|
||||
|
@ -100,6 +100,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
public CarouselItem? Item { get; set; }
|
||||
public BindableBool Selected { get; } = new BindableBool();
|
||||
public BindableBool Expanded { get; } = new BindableBool();
|
||||
public BindableBool KeyboardSelected { get; } = new BindableBool();
|
||||
|
||||
public double DrawYPosition { get; set; }
|
||||
|
@ -25,6 +25,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
private BeatmapCarousel carousel { get; set; } = null!;
|
||||
|
||||
private OsuSpriteText text = null!;
|
||||
private Box box = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
@ -34,7 +35,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
box = new Box
|
||||
{
|
||||
Colour = Color4.Yellow.Darken(5),
|
||||
Alpha = 0.8f,
|
||||
@ -48,6 +49,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
}
|
||||
};
|
||||
|
||||
Expanded.BindValueChanged(value =>
|
||||
{
|
||||
box.FadeColour(value.NewValue ? Color4.Yellow.Darken(2) : Color4.Yellow.Darken(5), 500, Easing.OutQuint);
|
||||
});
|
||||
|
||||
KeyboardSelected.BindValueChanged(value =>
|
||||
{
|
||||
if (value.NewValue)
|
||||
@ -67,7 +73,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
base.PrepareForUse();
|
||||
|
||||
Debug.Assert(Item != null);
|
||||
Debug.Assert(Item.IsGroupSelectionTarget);
|
||||
|
||||
var beatmapSetInfo = (BeatmapSetInfo)Item.Model;
|
||||
|
||||
@ -86,6 +91,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
public CarouselItem? Item { get; set; }
|
||||
public BindableBool Selected { get; } = new BindableBool();
|
||||
public BindableBool Expanded { get; } = new BindableBool();
|
||||
public BindableBool KeyboardSelected { get; } = new BindableBool();
|
||||
|
||||
public double DrawYPosition { get; set; }
|
||||
|
@ -8,6 +8,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -130,7 +131,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
///
|
||||
/// A filter may add, mutate or remove items.
|
||||
/// </remarks>
|
||||
protected IEnumerable<ICarouselFilter> Filters { get; init; } = Enumerable.Empty<ICarouselFilter>();
|
||||
public IEnumerable<ICarouselFilter> Filters { get; init; } = Enumerable.Empty<ICarouselFilter>();
|
||||
|
||||
/// <summary>
|
||||
/// All items which are to be considered for display in this carousel.
|
||||
@ -167,12 +168,18 @@ namespace osu.Game.Screens.SelectV2
|
||||
protected Drawable? GetMaterialisedDrawableForItem(CarouselItem item) =>
|
||||
scroll.Panels.SingleOrDefault(p => ((ICarouselPanel)p).Item == item);
|
||||
|
||||
/// <summary>
|
||||
/// When a user is traversing the carousel via group selection keys, assert whether the item provided is a valid target.
|
||||
/// </summary>
|
||||
/// <param name="item">The candidate item.</param>
|
||||
/// <returns>Whether the provided item is a valid group target. If <c>false</c>, more panels will be checked in the user's requested direction until a valid target is found.</returns>
|
||||
protected virtual bool CheckValidForGroupSelection(CarouselItem item) => true;
|
||||
|
||||
/// <summary>
|
||||
/// Called when an item is "selected".
|
||||
/// </summary>
|
||||
protected virtual void HandleItemSelected(object? model)
|
||||
{
|
||||
}
|
||||
/// <returns>Whether the item should be selected.</returns>
|
||||
protected virtual bool HandleItemSelected(object? model) => true;
|
||||
|
||||
/// <summary>
|
||||
/// Called when an item is "deselected".
|
||||
@ -205,7 +212,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
InternalChild = scroll = new CarouselScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = false,
|
||||
};
|
||||
|
||||
Items.BindCollectionChanged((_, _) => FilterAsync());
|
||||
@ -303,19 +309,19 @@ namespace osu.Game.Screens.SelectV2
|
||||
return true;
|
||||
|
||||
case GlobalAction.SelectNext:
|
||||
selectNext(1, isGroupSelection: false);
|
||||
return true;
|
||||
|
||||
case GlobalAction.SelectNextGroup:
|
||||
selectNext(1, isGroupSelection: true);
|
||||
traverseKeyboardSelection(1);
|
||||
return true;
|
||||
|
||||
case GlobalAction.SelectPrevious:
|
||||
selectNext(-1, isGroupSelection: false);
|
||||
traverseKeyboardSelection(-1);
|
||||
return true;
|
||||
|
||||
case GlobalAction.SelectNextGroup:
|
||||
traverseGroupSelection(1);
|
||||
return true;
|
||||
|
||||
case GlobalAction.SelectPreviousGroup:
|
||||
selectNext(-1, isGroupSelection: true);
|
||||
traverseGroupSelection(-1);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -326,91 +332,99 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Select the next valid selection relative to a current selection.
|
||||
/// This is generally for keyboard based traversal.
|
||||
/// </summary>
|
||||
/// <param name="direction">Positive for downwards, negative for upwards.</param>
|
||||
/// <param name="isGroupSelection">Whether the selection should traverse groups. Group selection updates the actual selection immediately, while non-group selection will only prepare a future keyboard selection.</param>
|
||||
/// <returns>Whether selection was possible.</returns>
|
||||
private bool selectNext(int direction, bool isGroupSelection)
|
||||
private void traverseKeyboardSelection(int direction)
|
||||
{
|
||||
// Ensure sanity
|
||||
Debug.Assert(direction != 0);
|
||||
direction = direction > 0 ? 1 : -1;
|
||||
if (carouselItems == null || carouselItems.Count == 0) return;
|
||||
|
||||
if (carouselItems == null || carouselItems.Count == 0)
|
||||
return false;
|
||||
int originalIndex;
|
||||
|
||||
// If the user has a different keyboard selection and requests
|
||||
// group selection, first transfer the keyboard selection to actual selection.
|
||||
if (isGroupSelection && currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem)
|
||||
{
|
||||
TryActivateSelection();
|
||||
return true;
|
||||
}
|
||||
if (currentKeyboardSelection.Index != null)
|
||||
originalIndex = currentKeyboardSelection.Index.Value;
|
||||
else if (direction > 0)
|
||||
originalIndex = carouselItems.Count - 1;
|
||||
else
|
||||
originalIndex = 0;
|
||||
|
||||
CarouselItem? selectionItem = currentKeyboardSelection.CarouselItem;
|
||||
int selectionIndex = currentKeyboardSelection.Index ?? -1;
|
||||
|
||||
// To keep things simple, let's first handle the cases where there's no selection yet.
|
||||
if (selectionItem == null || selectionIndex < 0)
|
||||
{
|
||||
// Start by selecting the first item.
|
||||
selectionItem = carouselItems.First();
|
||||
selectionIndex = 0;
|
||||
|
||||
// In the forwards case, immediately attempt selection of this panel.
|
||||
// If selection fails, continue with standard logic to find the next valid selection.
|
||||
if (direction > 0 && attemptSelection(selectionItem))
|
||||
return true;
|
||||
|
||||
// In the backwards direction we can just allow the selection logic to go ahead and loop around to the last valid.
|
||||
}
|
||||
|
||||
Debug.Assert(selectionItem != null);
|
||||
|
||||
// As a second special case, if we're group selecting backwards and the current selection isn't a group,
|
||||
// make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early.
|
||||
if (isGroupSelection && direction < 0)
|
||||
{
|
||||
while (!carouselItems[selectionIndex].IsGroupSelectionTarget)
|
||||
selectionIndex--;
|
||||
}
|
||||
|
||||
CarouselItem? newItem;
|
||||
int newIndex = originalIndex;
|
||||
|
||||
// Iterate over every item back to the current selection, finding the first valid item.
|
||||
// The fail condition is when we reach the selection after a cyclic loop over every item.
|
||||
do
|
||||
{
|
||||
selectionIndex += direction;
|
||||
newItem = carouselItems[(selectionIndex + carouselItems.Count) % carouselItems.Count];
|
||||
newIndex = (newIndex + direction + carouselItems.Count) % carouselItems.Count;
|
||||
var newItem = carouselItems[newIndex];
|
||||
|
||||
if (attemptSelection(newItem))
|
||||
return true;
|
||||
} while (newItem != selectionItem);
|
||||
if (newItem.IsVisible)
|
||||
{
|
||||
setKeyboardSelection(newItem.Model);
|
||||
return;
|
||||
}
|
||||
} while (newIndex != originalIndex);
|
||||
}
|
||||
|
||||
return false;
|
||||
/// <summary>
|
||||
/// Select the next valid selection relative to a current selection.
|
||||
/// This is generally for keyboard based traversal.
|
||||
/// </summary>
|
||||
/// <param name="direction">Positive for downwards, negative for upwards.</param>
|
||||
/// <returns>Whether selection was possible.</returns>
|
||||
private void traverseGroupSelection(int direction)
|
||||
{
|
||||
if (carouselItems == null || carouselItems.Count == 0) return;
|
||||
|
||||
bool attemptSelection(CarouselItem item)
|
||||
// If the user has a different keyboard selection and requests
|
||||
// group selection, first transfer the keyboard selection to actual selection.
|
||||
if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem)
|
||||
{
|
||||
if (!item.IsVisible || (isGroupSelection && !item.IsGroupSelectionTarget))
|
||||
return false;
|
||||
TryActivateSelection();
|
||||
|
||||
if (isGroupSelection)
|
||||
setSelection(item.Model);
|
||||
else
|
||||
setKeyboardSelection(item.Model);
|
||||
|
||||
return true;
|
||||
// There's a chance this couldn't resolve, at which point continue with standard traversal.
|
||||
if (currentSelection.CarouselItem == currentKeyboardSelection.CarouselItem)
|
||||
return;
|
||||
}
|
||||
|
||||
int originalIndex;
|
||||
int newIndex;
|
||||
|
||||
if (currentSelection.Index == null)
|
||||
{
|
||||
// If there's no current selection, start from either end of the full list.
|
||||
newIndex = originalIndex = direction > 0 ? carouselItems.Count - 1 : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
newIndex = originalIndex = currentSelection.Index.Value;
|
||||
|
||||
// As a second special case, if we're group selecting backwards and the current selection isn't a group,
|
||||
// make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early.
|
||||
if (direction < 0)
|
||||
{
|
||||
while (!CheckValidForGroupSelection(carouselItems[newIndex]))
|
||||
newIndex--;
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over every item back to the current selection, finding the first valid item.
|
||||
// The fail condition is when we reach the selection after a cyclic loop over every item.
|
||||
do
|
||||
{
|
||||
newIndex = (newIndex + direction + carouselItems.Count) % carouselItems.Count;
|
||||
var newItem = carouselItems[newIndex];
|
||||
|
||||
if (CheckValidForGroupSelection(newItem))
|
||||
{
|
||||
setSelection(newItem.Model);
|
||||
return;
|
||||
}
|
||||
} while (newIndex != originalIndex);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Selection handling
|
||||
|
||||
private readonly Cached selectionValid = new Cached();
|
||||
|
||||
private Selection currentKeyboardSelection = new Selection();
|
||||
private Selection currentSelection = new Selection();
|
||||
|
||||
@ -419,29 +433,22 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (currentSelection.Model == model)
|
||||
return;
|
||||
|
||||
var previousSelection = currentSelection;
|
||||
if (HandleItemSelected(model))
|
||||
{
|
||||
if (currentSelection.Model != null)
|
||||
HandleItemDeselected(currentSelection.Model);
|
||||
|
||||
if (previousSelection.Model != null)
|
||||
HandleItemDeselected(previousSelection.Model);
|
||||
currentKeyboardSelection = new Selection(model);
|
||||
currentSelection = currentKeyboardSelection;
|
||||
}
|
||||
|
||||
currentSelection = currentKeyboardSelection = new Selection(model);
|
||||
HandleItemSelected(currentSelection.Model);
|
||||
|
||||
// `HandleItemSelected` can alter `CurrentSelection`, which will recursively call `setSelection()` again.
|
||||
// if that happens, the rest of this method should be a no-op.
|
||||
if (currentSelection.Model != model)
|
||||
return;
|
||||
|
||||
refreshAfterSelection();
|
||||
scrollToSelection();
|
||||
selectionValid.Invalidate();
|
||||
}
|
||||
|
||||
private void setKeyboardSelection(object? model)
|
||||
{
|
||||
currentKeyboardSelection = new Selection(model);
|
||||
|
||||
refreshAfterSelection();
|
||||
scrollToSelection();
|
||||
selectionValid.Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -526,6 +533,13 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (carouselItems == null)
|
||||
return;
|
||||
|
||||
if (!selectionValid.IsValid)
|
||||
{
|
||||
refreshAfterSelection();
|
||||
scrollToSelection();
|
||||
selectionValid.Validate();
|
||||
}
|
||||
|
||||
var range = getDisplayRange();
|
||||
|
||||
if (range != displayedRange)
|
||||
@ -544,8 +558,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (c.Item == null)
|
||||
continue;
|
||||
|
||||
if (panel.Depth != c.DrawYPosition)
|
||||
scroll.Panels.ChangeChildDepth(panel, (float)c.DrawYPosition);
|
||||
double selectedYPos = currentSelection?.CarouselItem?.CarouselYPosition ?? 0;
|
||||
scroll.Panels.ChangeChildDepth(panel, (float)Math.Abs(c.DrawYPosition - selectedYPos));
|
||||
|
||||
if (c.DrawYPosition != c.Item.CarouselYPosition)
|
||||
c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed);
|
||||
@ -557,6 +571,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
c.Selected.Value = c.Item == currentSelection?.CarouselItem;
|
||||
c.KeyboardSelected.Value = c.Item == currentKeyboardSelection?.CarouselItem;
|
||||
c.Expanded.Value = c.Item.IsExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
@ -660,6 +675,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
carouselPanel.Item = null;
|
||||
carouselPanel.Selected.Value = false;
|
||||
carouselPanel.KeyboardSelected.Value = false;
|
||||
carouselPanel.Expanded.Value = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -30,15 +30,15 @@ namespace osu.Game.Screens.SelectV2
|
||||
public float DrawHeight { get; set; } = DEFAULT_HEIGHT;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this item should be a valid target for user group selection hotkeys.
|
||||
/// </summary>
|
||||
public bool IsGroupSelectionTarget { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this item is visible or collapsed (hidden).
|
||||
/// Whether this item is visible or hidden.
|
||||
/// </summary>
|
||||
public bool IsVisible { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this item is expanded or not. Should only be used for headers of groups.
|
||||
/// </summary>
|
||||
public bool IsExpanded { get; set; }
|
||||
|
||||
public CarouselItem(object model)
|
||||
{
|
||||
Model = model;
|
||||
|
120
osu.Game/Screens/SelectV2/GroupPanel.cs
Normal file
120
osu.Game/Screens/SelectV2/GroupPanel.cs
Normal file
@ -0,0 +1,120 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class GroupPanel : PoolableDrawable, ICarouselPanel
|
||||
{
|
||||
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 2;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapCarousel carousel { get; set; } = null!;
|
||||
|
||||
private Box activationFlash = null!;
|
||||
private OsuSpriteText text = null!;
|
||||
|
||||
private Box box = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Size = new Vector2(500, HEIGHT);
|
||||
Masking = true;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
box = new Box
|
||||
{
|
||||
Colour = Color4.DarkBlue.Darken(5),
|
||||
Alpha = 0.8f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
activationFlash = new Box
|
||||
{
|
||||
Colour = Color4.White,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Alpha = 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
text = new OsuSpriteText
|
||||
{
|
||||
Padding = new MarginPadding(5),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
}
|
||||
};
|
||||
|
||||
Selected.BindValueChanged(value =>
|
||||
{
|
||||
activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint);
|
||||
});
|
||||
|
||||
Expanded.BindValueChanged(value =>
|
||||
{
|
||||
box.FadeColour(value.NewValue ? Color4.SkyBlue : Color4.DarkBlue.Darken(5), 500, Easing.OutQuint);
|
||||
});
|
||||
|
||||
KeyboardSelected.BindValueChanged(value =>
|
||||
{
|
||||
if (value.NewValue)
|
||||
{
|
||||
BorderThickness = 5;
|
||||
BorderColour = Color4.Pink;
|
||||
}
|
||||
else
|
||||
{
|
||||
BorderThickness = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void PrepareForUse()
|
||||
{
|
||||
base.PrepareForUse();
|
||||
|
||||
Debug.Assert(Item != null);
|
||||
|
||||
GroupDefinition group = (GroupDefinition)Item.Model;
|
||||
|
||||
text.Text = group.Title;
|
||||
|
||||
this.FadeInFromZero(500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
carousel.CurrentSelection = Item!.Model;
|
||||
return true;
|
||||
}
|
||||
|
||||
#region ICarouselPanel
|
||||
|
||||
public CarouselItem? Item { get; set; }
|
||||
public BindableBool Selected { get; } = new BindableBool();
|
||||
public BindableBool Expanded { get; } = new BindableBool();
|
||||
public BindableBool KeyboardSelected { get; } = new BindableBool();
|
||||
|
||||
public double DrawYPosition { get; set; }
|
||||
|
||||
public void Activated()
|
||||
{
|
||||
// sets should never be activated.
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -14,10 +14,15 @@ namespace osu.Game.Screens.SelectV2
|
||||
public interface ICarouselPanel
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this item has selection. Should be read from to update the visual state.
|
||||
/// Whether this item has selection (see <see cref="Carousel{T}.CurrentSelection"/>). Should be read from to update the visual state.
|
||||
/// </summary>
|
||||
BindableBool Selected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this item is expanded (see <see cref="CarouselItem.IsExpanded"/>). Should be read from to update the visual state.
|
||||
/// </summary>
|
||||
BindableBool Expanded { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this item has keyboard selection. Should be read from to update the visual state.
|
||||
/// </summary>
|
||||
|
@ -335,6 +335,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task ChangeUserStyle(int? beatmapId, int? rulesetId)
|
||||
{
|
||||
ChangeUserStyle(api.LocalUser.Value.Id, beatmapId, rulesetId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void ChangeUserStyle(int userId, int? beatmapId, int? rulesetId)
|
||||
{
|
||||
Debug.Assert(ServerRoom != null);
|
||||
|
||||
var user = ServerRoom.Users.Single(u => u.UserID == userId);
|
||||
user.BeatmapId = beatmapId;
|
||||
user.RulesetId = rulesetId;
|
||||
|
||||
((IMultiplayerClient)this).UserStyleChanged(userId, beatmapId, rulesetId);
|
||||
}
|
||||
|
||||
public void ChangeUserMods(int userId, IEnumerable<Mod> newMods)
|
||||
=> ChangeUserMods(userId, newMods.Select(m => new APIMod(m)));
|
||||
|
||||
|
@ -35,7 +35,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="20.1.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2025.129.1" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2025.204.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2025.129.0" />
|
||||
<PackageReference Include="Sentry" Version="5.0.0" />
|
||||
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||
|
@ -17,6 +17,6 @@
|
||||
<MtouchInterpreter>-all</MtouchInterpreter>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.129.1" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.204.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
Loading…
Reference in New Issue
Block a user