1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 15:47:26 +08:00

Merge remote-tracking branch 'refs/remotes/ppy/master' into subcomments-alter-new

This commit is contained in:
Andrei Zavatski 2020-02-14 20:55:51 +03:00
commit 7cb11be810
81 changed files with 1869 additions and 943 deletions

View File

@ -54,6 +54,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1230.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.207.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.213.0" />
</ItemGroup>
</Project>

View File

@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModDifficultyAdjust : ModDifficultyAdjust
{
[SettingSource("Fruit Size", "Override a beatmap's set CS.")]
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
public BindableNumber<float> CircleSize { get; } = new BindableFloat
{
Precision = 0.1f,
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Mods
Value = 5,
};
[SettingSource("Approach Rate", "Override a beatmap's set AR.")]
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
public BindableNumber<float> ApproachRate { get; } = new BindableFloat
{
Precision = 0.1f,

View File

@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.UpdatePosition(screenSpacePosition);
if (PlacementBegun)
if (PlacementActive)
{
var endTime = TimeAt(screenSpacePosition);

View File

@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
public override void UpdatePosition(Vector2 screenSpacePosition)
{
if (!PlacementBegun)
if (!PlacementActive)
Column = ColumnAt(screenSpacePosition);
if (Column == null) return;

View File

@ -7,7 +7,9 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing.Input;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
@ -21,12 +23,50 @@ namespace osu.Game.Rulesets.Osu.Tests
typeof(CursorTrail)
};
[BackgroundDependencyLoader]
private void load()
[Cached]
private GameplayBeatmap gameplayBeatmap;
private ClickingCursorContainer lastContainer;
[Resolved]
private OsuConfigManager config { get; set; }
public TestSceneGameplayCursor()
{
gameplayBeatmap = new GameplayBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
}
[TestCase(1, 1)]
[TestCase(5, 1)]
[TestCase(10, 1)]
[TestCase(1, 1.5f)]
[TestCase(5, 1.5f)]
[TestCase(10, 1.5f)]
public void TestSizing(int circleSize, float userScale)
{
AddStep($"set user scale to {userScale}", () => config.Set(OsuSetting.GameplayCursorSize, userScale));
AddStep($"adjust cs to {circleSize}", () => gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize);
AddStep("turn on autosizing", () => config.Set(OsuSetting.AutoCursorSize, true));
AddStep("load content", loadContent);
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale);
AddStep("set user scale to 1", () => config.Set(OsuSetting.GameplayCursorSize, 1f));
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize));
AddStep("turn off autosizing", () => config.Set(OsuSetting.AutoCursorSize, false));
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == 1);
AddStep($"set user scale to {userScale}", () => config.Set(OsuSetting.GameplayCursorSize, userScale));
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == userScale);
}
private void loadContent()
{
SetContents(() => new MovingCursorInputManager
{
Child = new ClickingCursorContainer
Child = lastContainer = new ClickingCursorContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,

View File

@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void beginCurve()
{
BeginPlacement();
BeginPlacement(commitStart: true);
setState(PlacementState.Body);
}

View File

@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
if (e.Button != MouseButton.Left)
return false;
BeginPlacement();
BeginPlacement(commitStart: true);
piece.FadeTo(1f, 150, Easing.OutQuint);
isPlacingEnd = true;

View File

@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.Edit
if (existing == null)
return;
hitObject.RemoveTransform(existing);
using (hitObject.BeginAbsoluteSequence(existing.StartTime))
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
break;

View File

@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModDifficultyAdjust : ModDifficultyAdjust
{
[SettingSource("Circle Size", "Override a beatmap's set CS.")]
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
public BindableNumber<float> CircleSize { get; } = new BindableFloat
{
Precision = 0.1f,
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
Value = 5,
};
[SettingSource("Approach Rate", "Override a beatmap's set AR.")]
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
public BindableNumber<float> ApproachRate { get; } = new BindableFloat
{
Precision = 0.1f,

View File

@ -19,33 +19,46 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
/// <summary>
/// How early before a hitobject's start time to trigger a hit.
/// </summary>
private const float relax_leniency = 3;
public void Update(Playfield playfield)
{
bool requiresHold = false;
bool requiresHit = false;
const float relax_leniency = 3;
double time = playfield.Clock.CurrentTime;
foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
foreach (var h in playfield.HitObjectContainer.AliveObjects.OfType<DrawableOsuHitObject>())
{
if (!(drawable is DrawableOsuHitObject osuHit))
// we are not yet close enough to the object.
if (time < h.HitObject.StartTime - relax_leniency)
break;
// already hit or beyond the hittable end time.
if (h.IsHit || (h.HitObject is IHasEndTime hasEnd && time > hasEnd.EndTime))
continue;
double time = osuHit.Clock.CurrentTime;
double relativetime = time - osuHit.HitObject.StartTime;
if (time < osuHit.HitObject.StartTime - relax_leniency) continue;
if ((osuHit.HitObject is IHasEndTime hasEnd && time > hasEnd.EndTime) || osuHit.IsHit)
continue;
if (osuHit is DrawableHitCircle && osuHit.IsHovered)
switch (h)
{
Debug.Assert(osuHit.HitObject.HitWindows != null);
requiresHit |= osuHit.HitObject.HitWindows.CanBeHit(relativetime);
}
case DrawableHitCircle circle:
handleHitCircle(circle);
break;
requiresHold |= (osuHit is DrawableSlider slider && (slider.Ball.IsHovered || osuHit.IsHovered)) || osuHit is DrawableSpinner;
case DrawableSlider slider:
// Handles cases like "2B" beatmaps, where sliders may be overlapping and simply holding is not enough.
if (!slider.HeadCircle.IsHit)
handleHitCircle(slider.HeadCircle);
requiresHold |= slider.Ball.IsHovered || h.IsHovered;
break;
case DrawableSpinner _:
requiresHold = true;
break;
}
}
if (requiresHit)
@ -55,6 +68,15 @@ namespace osu.Game.Rulesets.Osu.Mods
}
addAction(requiresHold);
void handleHitCircle(DrawableHitCircle circle)
{
if (!circle.IsHovered)
return;
Debug.Assert(circle.HitObject.HitWindows != null);
requiresHit |= circle.HitObject.HitWindows.CanBeHit(time - circle.HitObject.StartTime);
}
}
private bool wasHit;

View File

@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osuTK;
@ -29,10 +30,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private readonly Drawable cursorTrail;
public Bindable<float> CursorScale;
public Bindable<float> CursorScale = new BindableFloat(1);
private Bindable<float> userCursorScale;
private Bindable<bool> autoCursorScale;
private readonly IBindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
public OsuCursorContainer()
{
@ -43,37 +44,16 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
};
}
[Resolved(canBeNull: true)]
private GameplayBeatmap beatmap { get; set; }
[Resolved]
private OsuConfigManager config { get; set; }
[BackgroundDependencyLoader(true)]
private void load(OsuConfigManager config, OsuRulesetConfigManager rulesetConfig, IBindable<WorkingBeatmap> beatmap)
private void load(OsuConfigManager config, OsuRulesetConfigManager rulesetConfig)
{
rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorTrail, showTrail);
this.beatmap.BindTo(beatmap);
this.beatmap.ValueChanged += _ => calculateScale();
userCursorScale = config.GetBindable<float>(OsuSetting.GameplayCursorSize);
userCursorScale.ValueChanged += _ => calculateScale();
autoCursorScale = config.GetBindable<bool>(OsuSetting.AutoCursorSize);
autoCursorScale.ValueChanged += _ => calculateScale();
CursorScale = new BindableFloat();
CursorScale.ValueChanged += e => ActiveCursor.Scale = cursorTrail.Scale = new Vector2(e.NewValue);
calculateScale();
}
private void calculateScale()
{
float scale = userCursorScale.Value;
if (autoCursorScale.Value && beatmap.Value != null)
{
// if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
scale *= 1f - 0.7f * (1f + beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY;
}
CursorScale.Value = scale;
}
protected override void LoadComplete()
@ -81,6 +61,46 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
base.LoadComplete();
showTrail.BindValueChanged(v => cursorTrail.FadeTo(v.NewValue ? 1 : 0, 200), true);
userCursorScale = config.GetBindable<float>(OsuSetting.GameplayCursorSize);
userCursorScale.ValueChanged += _ => calculateScale();
autoCursorScale = config.GetBindable<bool>(OsuSetting.AutoCursorSize);
autoCursorScale.ValueChanged += _ => calculateScale();
CursorScale.ValueChanged += e =>
{
var newScale = new Vector2(e.NewValue);
ActiveCursor.Scale = newScale;
cursorTrail.Scale = newScale;
};
calculateScale();
}
/// <summary>
/// Get the scale applicable to the ActiveCursor based on a beatmap's circle size.
/// </summary>
public static float GetScaleForCircleSize(float circleSize) =>
1f - 0.7f * (1f + circleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY;
private void calculateScale()
{
float scale = userCursorScale.Value;
if (autoCursorScale.Value && beatmap != null)
{
// if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
scale *= GetScaleForCircleSize(beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
}
CursorScale.Value = scale;
var newScale = new Vector2(scale);
ActiveCursor.ScaleTo(newScale, 400, Easing.OutQuint);
cursorTrail.Scale = newScale;
}
private int downCount;

View File

@ -121,10 +121,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
room.Playlist.Add(new PlaylistItem
{
Ruleset = ruleset,
Beatmap = new BeatmapInfo
Ruleset = { Value = ruleset },
Beatmap =
{
Metadata = new BeatmapMetadata()
Value = new BeatmapInfo
{
Metadata = new BeatmapMetadata()
}
}
});
}

View File

@ -7,7 +7,6 @@ using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Match.Components;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Framework.Allocation;
@ -32,22 +31,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
Origin = Anchor.Centre,
});
Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 1763072 } });
Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 2101557 } });
Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 1973466 } });
Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 2109801 } });
Room.Playlist.Add(new PlaylistItem { Beatmap = new BeatmapInfo { OnlineBeatmapID = 1922035 } });
}
protected override void LoadComplete()
{
base.LoadComplete();
AddStep("Select random beatmap", () =>
{
Room.CurrentItem.Value = Room.Playlist[RNG.Next(Room.Playlist.Count)];
previewTrackManager.StopAnyPlaying(this);
});
Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = new BeatmapInfo { OnlineBeatmapID = 1763072 } } });
Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = new BeatmapInfo { OnlineBeatmapID = 2101557 } } });
Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = new BeatmapInfo { OnlineBeatmapID = 1973466 } } });
Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = new BeatmapInfo { OnlineBeatmapID = 2109801 } } });
Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = new BeatmapInfo { OnlineBeatmapID = 1922035 } } });
}
}
}

View File

@ -23,16 +23,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Room.Playlist.Add(new PlaylistItem
{
Beatmap = new BeatmapInfo
Beatmap =
{
Metadata = new BeatmapMetadata
Value = new BeatmapInfo
{
Title = "Title",
Artist = "Artist",
AuthorString = "Author",
},
Version = "Version",
Ruleset = new OsuRuleset().RulesetInfo
Metadata = new BeatmapMetadata
{
Title = "Title",
Artist = "Artist",
AuthorString = "Author",
},
Version = "Version",
Ruleset = new OsuRuleset().RulesetInfo
}
},
RequiredMods =
{

View File

@ -37,16 +37,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
Room.Playlist.Clear();
Room.Playlist.Add(new PlaylistItem
{
Beatmap = new BeatmapInfo
Beatmap =
{
StarDifficulty = 2.4,
Ruleset = rulesets.GetRuleset(0),
Metadata = new BeatmapMetadata
Value = new BeatmapInfo
{
Title = @"My Song",
Artist = @"VisualTests",
AuthorString = @"osu!lazer",
},
StarDifficulty = 2.4,
Ruleset = rulesets.GetRuleset(0),
Metadata = new BeatmapMetadata
{
Title = @"My Song",
Artist = @"VisualTests",
AuthorString = @"osu!lazer",
},
}
}
});
});
@ -60,16 +63,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
Room.Playlist.Clear();
Room.Playlist.Add(new PlaylistItem
{
Beatmap = new BeatmapInfo
Beatmap =
{
StarDifficulty = 4.2,
Ruleset = rulesets.GetRuleset(3),
Metadata = new BeatmapMetadata
Value = new BeatmapInfo
{
Title = @"Your Song",
Artist = @"Tester",
AuthorString = @"Someone",
},
StarDifficulty = 4.2,
Ruleset = rulesets.GetRuleset(3),
Metadata = new BeatmapMetadata
{
Title = @"Your Song",
Artist = @"Tester",
AuthorString = @"Someone",
},
}
}
});
});

View File

@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Add(new Participants { RelativeSizeAxes = Axes.Both });
AddStep(@"set max to null", () => Room.MaxParticipants.Value = null);
AddStep(@"set users", () => Room.Participants.Value = new[]
AddStep(@"set users", () => Room.Participants.AddRange(new[]
{
new User
{
@ -42,10 +42,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/5287410/5cfeaa9dd41cbce038ecdc9d781396ed4b0108089170bf7f50492ef8eadeb368.jpeg",
IsSupporter = true,
},
});
}));
AddStep(@"set max", () => Room.MaxParticipants.Value = 10);
AddStep(@"clear users", () => Room.Participants.Value = System.Array.Empty<User>());
AddStep(@"clear users", () => Room.Participants.Clear());
AddStep(@"set max to null", () => Room.MaxParticipants.Value = null);
}
}

View File

@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("set name", () => Room.Name.Value = "Room name");
AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value);
AddStep("set beatmap", () => Room.Playlist.Add(new PlaylistItem { Beatmap = CreateBeatmap(Ruleset.Value).BeatmapInfo }));
AddStep("set beatmap", () => Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }));
AddAssert("button enabled", () => settings.ApplyButton.Enabled.Value);
AddStep("clear name", () => Room.Name.Value = "");

View File

@ -0,0 +1,97 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public class TestSceneOnlineViewContainer : OsuTestScene
{
private readonly TestOnlineViewContainer onlineView;
public TestSceneOnlineViewContainer()
{
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Child = onlineView = new TestOnlineViewContainer()
};
}
[Test]
public void TestOnlineStateVisibility()
{
AddStep("set status to online", () => ((DummyAPIAccess)API).State = APIState.Online);
AddUntilStep("children are visible", () => onlineView.ViewTarget.IsPresent);
AddUntilStep("loading animation is not visible", () => !onlineView.LoadingAnimation.IsPresent);
}
[Test]
public void TestOfflineStateVisibility()
{
AddStep("set status to offline", () => ((DummyAPIAccess)API).State = APIState.Offline);
AddUntilStep("children are not visible", () => !onlineView.ViewTarget.IsPresent);
AddUntilStep("loading animation is not visible", () => !onlineView.LoadingAnimation.IsPresent);
}
[Test]
public void TestConnectingStateVisibility()
{
AddStep("set status to connecting", () => ((DummyAPIAccess)API).State = APIState.Connecting);
AddUntilStep("children are not visible", () => !onlineView.ViewTarget.IsPresent);
AddUntilStep("loading animation is visible", () => onlineView.LoadingAnimation.IsPresent);
}
[Test]
public void TestFailingStateVisibility()
{
AddStep("set status to failing", () => ((DummyAPIAccess)API).State = APIState.Failing);
AddUntilStep("children are not visible", () => !onlineView.ViewTarget.IsPresent);
AddUntilStep("loading animation is visible", () => onlineView.LoadingAnimation.IsPresent);
}
private class TestOnlineViewContainer : OnlineViewContainer
{
public new LoadingAnimation LoadingAnimation => base.LoadingAnimation;
public CompositeDrawable ViewTarget => base.Content;
public TestOnlineViewContainer()
: base(@"Please sign in to view dummy test content")
{
RelativeSizeAxes = Axes.Both;
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Blue.Opacity(0.8f),
},
new OsuSpriteText
{
Text = "dummy online content",
Font = OsuFont.Default.With(size: 40),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
}
}
}
}

View File

@ -34,25 +34,7 @@ namespace osu.Game.Tests.Visual.Online
{
Current = { BindTarget = scope },
Country = { BindTarget = countryBindable },
Ruleset = { BindTarget = ruleset },
Spotlights = new[]
{
new Spotlight
{
Id = 1,
Text = "Spotlight 1"
},
new Spotlight
{
Id = 2,
Text = "Spotlight 2"
},
new Spotlight
{
Id = 3,
Text = "Spotlight 3"
}
}
Ruleset = { BindTarget = ruleset }
});
var country = new Country

View File

@ -35,6 +35,12 @@ namespace osu.Game.Tests.Visual.Online
Add(selector = new SpotlightSelector());
}
[Test]
public void TestVisibility()
{
AddStep("Toggle Visibility", selector.ToggleVisibility);
}
[Test]
public void TestLocalSpotlights()
{

View File

@ -0,0 +1,55 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Overlays.Rankings;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneSpotlightsLayout : OsuTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(SpotlightsLayout),
typeof(SpotlightSelector),
};
protected override bool UseOnlineAPI => true;
[Cached]
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Green);
public TestSceneSpotlightsLayout()
{
var ruleset = new Bindable<RulesetInfo>(new OsuRuleset().RulesetInfo);
Add(new BasicScrollContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Width = 0.8f,
Child = new SpotlightsLayout
{
Ruleset = { BindTarget = ruleset }
}
});
AddStep("Osu ruleset", () => ruleset.Value = new OsuRuleset().RulesetInfo);
AddStep("Mania ruleset", () => ruleset.Value = new ManiaRuleset().RulesetInfo);
AddStep("Taiko ruleset", () => ruleset.Value = new TaikoRuleset().RulesetInfo);
AddStep("Catch ruleset", () => ruleset.Value = new CatchRuleset().RulesetInfo);
}
}
}

View File

@ -0,0 +1,40 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneModDisplay : OsuTestScene
{
[TestCase(ExpansionMode.ExpandOnHover)]
[TestCase(ExpansionMode.AlwaysExpanded)]
[TestCase(ExpansionMode.AlwaysContracted)]
public void TestMode(ExpansionMode mode)
{
AddStep("create mod display", () =>
{
Child = new ModDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
ExpansionMode = mode,
Current =
{
Value = new Mod[]
{
new OsuModHardRock(),
new OsuModDoubleTime(),
new OsuModDifficultyAdjust(),
new OsuModEasy(),
}
}
};
});
}
}
}

View File

@ -37,9 +37,9 @@ namespace osu.Game.Tests
trackStore = audioManager.GetTrackStore(getZipReader());
}
protected override void Dispose(bool isDisposing)
~WaveformTestBeatmap()
{
base.Dispose(isDisposing);
// Remove the track store from the audio manager
trackStore?.Dispose();
}

View File

@ -36,8 +36,9 @@ namespace osu.Game.Beatmaps
using (var stream = new LineBufferedReader(store.GetStream(getPathForFile(BeatmapInfo.Path))))
return Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
}
catch
catch (Exception e)
{
Logger.Error(e, "Beatmap failed to load");
return null;
}
}
@ -59,8 +60,9 @@ namespace osu.Game.Beatmaps
{
return textureStore.Get(getPathForFile(Metadata.BackgroundFile));
}
catch
catch (Exception e)
{
Logger.Error(e, "Background failed to load");
return null;
}
}
@ -74,8 +76,9 @@ namespace osu.Game.Beatmaps
{
return new VideoSprite(textureStore.GetStream(getPathForFile(Metadata.VideoFile)));
}
catch
catch (Exception e)
{
Logger.Error(e, "Video failed to load");
return null;
}
}
@ -86,8 +89,9 @@ namespace osu.Game.Beatmaps
{
return (trackStore ??= AudioManager.GetTrackStore(store)).Get(getPathForFile(Metadata.AudioFile));
}
catch
catch (Exception e)
{
Logger.Error(e, "Track failed to load");
return null;
}
}
@ -115,8 +119,9 @@ namespace osu.Game.Beatmaps
var trackData = store.GetStream(getPathForFile(Metadata.AudioFile));
return trackData == null ? null : new Waveform(trackData);
}
catch
catch (Exception e)
{
Logger.Error(e, "Waveform failed to load");
return null;
}
}

View File

@ -17,10 +17,11 @@ using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
using osu.Framework.Graphics.Video;
using osu.Framework.Logging;
namespace osu.Game.Beatmaps
{
public abstract class WorkingBeatmap : IWorkingBeatmap, IDisposable
public abstract class WorkingBeatmap : IWorkingBeatmap
{
public readonly BeatmapInfo BeatmapInfo;
@ -133,11 +134,29 @@ namespace osu.Game.Beatmaps
return converted;
}
public override string ToString() => BeatmapInfo.ToString();
private CancellationTokenSource loadCancellation = new CancellationTokenSource();
public bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
/// <summary>
/// Beings loading the contents of this <see cref="WorkingBeatmap"/> asynchronously.
/// </summary>
public void BeginAsyncLoad()
{
loadBeatmapAsync();
}
public Task<IBeatmap> LoadBeatmapAsync() => beatmapLoadTask ??= Task.Factory.StartNew(() =>
/// <summary>
/// Cancels the asynchronous loading of the contents of this <see cref="WorkingBeatmap"/>.
/// </summary>
public void CancelAsyncLoad()
{
loadCancellation?.Cancel();
loadCancellation = new CancellationTokenSource();
if (beatmapLoadTask?.IsCompleted != true)
beatmapLoadTask = null;
}
private Task<IBeatmap> loadBeatmapAsync() => beatmapLoadTask ??= Task.Factory.StartNew(() =>
{
// Todo: Handle cancellation during beatmap parsing
var b = GetBeatmap() ?? new Beatmap();
@ -149,7 +168,11 @@ namespace osu.Game.Beatmaps
b.BeatmapInfo = BeatmapInfo;
return b;
}, beatmapCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
}, loadCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
public override string ToString() => BeatmapInfo.ToString();
public bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
public IBeatmap Beatmap
{
@ -157,16 +180,25 @@ namespace osu.Game.Beatmaps
{
try
{
return LoadBeatmapAsync().Result;
return loadBeatmapAsync().Result;
}
catch (TaskCanceledException)
catch (AggregateException ae)
{
// This is the exception that is generally expected here, which occurs via natural cancellation of the asynchronous load
if (ae.InnerExceptions.FirstOrDefault() is TaskCanceledException)
return null;
Logger.Error(ae, "Beatmap failed to load");
return null;
}
catch (Exception e)
{
Logger.Error(e, "Beatmap failed to load");
return null;
}
}
}
private readonly CancellationTokenSource beatmapCancellation = new CancellationTokenSource();
protected abstract IBeatmap GetBeatmap();
private Task<IBeatmap> beatmapLoadTask;
@ -217,40 +249,11 @@ namespace osu.Game.Beatmaps
/// </summary>
public virtual void RecycleTrack() => track.Recycle();
#region Disposal
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private bool isDisposed;
protected virtual void Dispose(bool isDisposing)
{
if (isDisposed)
return;
isDisposed = true;
// recycling logic is not here for the time being, as components which use
// retrieved objects from WorkingBeatmap may not hold a reference to the WorkingBeatmap itself.
// this should be fine as each retrieved component do have their own finalizers.
// cancelling the beatmap load is safe for now since the retrieval is a synchronous
// operation. if we add an async retrieval method this may need to be reconsidered.
beatmapCancellation?.Cancel();
total_count.Value--;
}
~WorkingBeatmap()
{
Dispose(false);
total_count.Value--;
}
#endregion
public class RecyclableLazy<T>
{
private Lazy<T> lazy;

View File

@ -20,7 +20,7 @@ namespace osu.Game.Configuration
Set(OsuSetting.Ruleset, 0, 0, int.MaxValue);
Set(OsuSetting.Skin, 0, -1, int.MaxValue);
Set(OsuSetting.BeatmapDetailTab, BeatmapDetailTab.Details);
Set(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details);
Set(OsuSetting.ShowConvertedBeatmaps, true);
Set(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1);

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using osu.Framework.Bindables;
@ -16,6 +17,10 @@ namespace osu.Game.Configuration
/// An attribute to mark a bindable as being exposed to the user via settings controls.
/// Can be used in conjunction with <see cref="SettingSourceExtensions.CreateSettingsControls"/> to automatically create UI controls.
/// </summary>
/// <remarks>
/// All controls with <see cref="OrderPosition"/> set will be placed first in ascending order.
/// All controls with no <see cref="OrderPosition"/> will come afterward in default order.
/// </remarks>
[MeansImplicitUse]
[AttributeUsage(AttributeTargets.Property)]
public class SettingSourceAttribute : Attribute
@ -24,18 +29,26 @@ namespace osu.Game.Configuration
public string Description { get; }
public int? OrderPosition { get; }
public SettingSourceAttribute(string label, string description = null)
{
Label = label ?? string.Empty;
Description = description ?? string.Empty;
}
public SettingSourceAttribute(string label, string description, int orderPosition)
: this(label, description)
{
OrderPosition = orderPosition;
}
}
public static class SettingSourceExtensions
{
public static IEnumerable<Drawable> CreateSettingsControls(this object obj)
{
foreach (var (attr, property) in obj.GetSettingsSourceProperties())
foreach (var (attr, property) in obj.GetOrderedSettingsSourceProperties())
{
object value = property.GetValue(obj);
@ -116,5 +129,15 @@ namespace osu.Game.Configuration
yield return (attr, property);
}
}
public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetOrderedSettingsSourceProperties(this object obj)
{
var original = obj.GetSettingsSourceProperties();
var orderedRelative = original.Where(attr => attr.Item1.OrderPosition != null).OrderBy(attr => attr.Item1.OrderPosition);
var unordered = original.Except(orderedRelative);
return orderedRelative.Concat(unordered);
}
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Globalization;
using osu.Game.Beatmaps;
using osuTK.Graphics;
@ -14,41 +15,40 @@ namespace osu.Game.Graphics
public static Color4 FromHex(string hex)
{
if (hex[0] == '#')
hex = hex.Substring(1);
var hexSpan = hex[0] == '#' ? hex.AsSpan().Slice(1) : hex.AsSpan();
switch (hex.Length)
switch (hexSpan.Length)
{
default:
throw new ArgumentException(@"Invalid hex string length!");
case 3:
return new Color4(
(byte)(Convert.ToByte(hex.Substring(0, 1), 16) * 17),
(byte)(Convert.ToByte(hex.Substring(1, 1), 16) * 17),
(byte)(Convert.ToByte(hex.Substring(2, 1), 16) * 17),
(byte)(byte.Parse(hexSpan.Slice(0, 1), NumberStyles.HexNumber) * 17),
(byte)(byte.Parse(hexSpan.Slice(1, 1), NumberStyles.HexNumber) * 17),
(byte)(byte.Parse(hexSpan.Slice(2, 1), NumberStyles.HexNumber) * 17),
255);
case 6:
return new Color4(
Convert.ToByte(hex.Substring(0, 2), 16),
Convert.ToByte(hex.Substring(2, 2), 16),
Convert.ToByte(hex.Substring(4, 2), 16),
byte.Parse(hexSpan.Slice(0, 2), NumberStyles.HexNumber),
byte.Parse(hexSpan.Slice(2, 2), NumberStyles.HexNumber),
byte.Parse(hexSpan.Slice(4, 2), NumberStyles.HexNumber),
255);
case 4:
return new Color4(
(byte)(Convert.ToByte(hex.Substring(0, 1), 16) * 17),
(byte)(Convert.ToByte(hex.Substring(1, 1), 16) * 17),
(byte)(Convert.ToByte(hex.Substring(2, 1), 16) * 17),
(byte)(Convert.ToByte(hex.Substring(3, 1), 16) * 17));
(byte)(byte.Parse(hexSpan.Slice(0, 1), NumberStyles.HexNumber) * 17),
(byte)(byte.Parse(hexSpan.Slice(1, 1), NumberStyles.HexNumber) * 17),
(byte)(byte.Parse(hexSpan.Slice(0, 1), NumberStyles.HexNumber) * 17),
(byte)(byte.Parse(hexSpan.Slice(0, 1), NumberStyles.HexNumber) * 17));
case 8:
return new Color4(
Convert.ToByte(hex.Substring(0, 2), 16),
Convert.ToByte(hex.Substring(2, 2), 16),
Convert.ToByte(hex.Substring(4, 2), 16),
Convert.ToByte(hex.Substring(6, 2), 16));
byte.Parse(hexSpan.Slice(0, 2), NumberStyles.HexNumber),
byte.Parse(hexSpan.Slice(2, 2), NumberStyles.HexNumber),
byte.Parse(hexSpan.Slice(4, 2), NumberStyles.HexNumber),
byte.Parse(hexSpan.Slice(6, 2), NumberStyles.HexNumber));
}
}

View File

@ -33,7 +33,7 @@ namespace osu.Game.Online.API
public APIState State
{
get => state;
private set
set
{
if (state == value)
return;

View File

@ -26,6 +26,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"end_date")]
public DateTimeOffset EndDate;
[JsonProperty(@"participant_count")]
public int? Participants;
public override string ToString() => Name;
}
}

View File

@ -152,7 +152,7 @@ namespace osu.Game.Online.Leaderboards
break;
case PlaceholderState.NotLoggedIn:
replacePlaceholder(new LoginPlaceholder());
replacePlaceholder(new LoginPlaceholder(@"Please sign in to view online leaderboards!"));
break;
case PlaceholderState.NotSupporter:

View File

@ -55,7 +55,7 @@ namespace osu.Game.Online.Leaderboards
private List<ScoreComponentLabel> statisticsLabels;
[Resolved]
[Resolved(CanBeNull = true)]
private DialogOverlay dialogOverlay { get; set; }
public LeaderboardScore(ScoreInfo score, int rank, bool allowHighlight = true)

View File

@ -1,9 +1,10 @@
// 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;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
@ -12,7 +13,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Online.Multiplayer
{
public class PlaylistItem
public class PlaylistItem : IEquatable<PlaylistItem>
{
[JsonProperty("id")]
public int ID { get; set; }
@ -24,24 +25,16 @@ namespace osu.Game.Online.Multiplayer
public int RulesetID { get; set; }
[JsonIgnore]
public BeatmapInfo Beatmap
{
get => beatmap;
set
{
beatmap = value;
BeatmapID = value?.OnlineBeatmapID ?? 0;
}
}
public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
[JsonIgnore]
public RulesetInfo Ruleset { get; set; }
public readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
[JsonIgnore]
public readonly List<Mod> AllowedMods = new List<Mod>();
public readonly BindableList<Mod> AllowedMods = new BindableList<Mod>();
[JsonIgnore]
public readonly List<Mod> RequiredMods = new List<Mod>();
public readonly BindableList<Mod> RequiredMods = new BindableList<Mod>();
[JsonProperty("beatmap")]
private APIBeatmap apiBeatmap { get; set; }
@ -64,16 +57,20 @@ namespace osu.Game.Online.Multiplayer
set => requiredModsBacking = value;
}
private BeatmapInfo beatmap;
public PlaylistItem()
{
Beatmap.BindValueChanged(beatmap => BeatmapID = beatmap.NewValue?.OnlineBeatmapID ?? 0);
Ruleset.BindValueChanged(ruleset => RulesetID = ruleset.NewValue?.ID ?? 0);
}
public void MapObjects(BeatmapManager beatmaps, RulesetStore rulesets)
{
// If we don't have an api beatmap, the request occurred as a result of room creation, so we can query the local beatmap instead
// Todo: Is this a bug? Room creation only returns the beatmap ID
Beatmap = apiBeatmap == null ? beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == BeatmapID) : apiBeatmap.ToBeatmap(rulesets);
Ruleset = rulesets.GetRuleset(RulesetID);
Beatmap.Value = apiBeatmap == null ? beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == BeatmapID) : apiBeatmap.ToBeatmap(rulesets);
Ruleset.Value = rulesets.GetRuleset(RulesetID);
Ruleset rulesetInstance = Ruleset.CreateInstance();
Ruleset rulesetInstance = Ruleset.Value.CreateInstance();
if (allowedModsBacking != null)
{
@ -94,5 +91,14 @@ namespace osu.Game.Online.Multiplayer
public bool ShouldSerializeID() => false;
public bool ShouldSerializeapiBeatmap() => false;
public bool Equals(PlaylistItem other) => ID == other?.ID && BeatmapID == other.BeatmapID && RulesetID == other.RulesetID;
public override int GetHashCode()
{
// ReSharper disable NonReadonlyMemberInGetHashCode
return HashCode.Combine(ID, BeatmapID, RulesetID);
// ReSharper restore NonReadonlyMemberInGetHashCode
}
}
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Allocation;
@ -31,10 +30,6 @@ namespace osu.Game.Online.Multiplayer
[JsonProperty("playlist")]
public BindableList<PlaylistItem> Playlist { get; private set; } = new BindableList<PlaylistItem>();
[Cached]
[JsonIgnore]
public Bindable<PlaylistItem> CurrentItem { get; private set; } = new Bindable<PlaylistItem>();
[Cached]
[JsonProperty("channel_id")]
public Bindable<int> ChannelId { get; private set; } = new Bindable<int>();
@ -65,23 +60,11 @@ namespace osu.Game.Online.Multiplayer
[Cached]
[JsonIgnore]
public Bindable<IEnumerable<User>> Participants { get; private set; } = new Bindable<IEnumerable<User>>(Enumerable.Empty<User>());
public BindableList<User> Participants { get; private set; } = new BindableList<User>();
[Cached]
public Bindable<int> ParticipantCount { get; private set; } = new Bindable<int>();
public Room()
{
Playlist.ItemsAdded += updateCurrent;
Playlist.ItemsRemoved += updateCurrent;
updateCurrent(Playlist);
}
private void updateCurrent(IEnumerable<PlaylistItem> playlist)
{
CurrentItem.Value = playlist.FirstOrDefault();
}
// todo: TEMPORARY
[JsonProperty("participant_count")]
private int? participantCount
@ -130,17 +113,18 @@ namespace osu.Game.Online.Multiplayer
Type.Value = other.Type.Value;
MaxParticipants.Value = other.MaxParticipants.Value;
ParticipantCount.Value = other.ParticipantCount.Value;
Participants.Value = other.Participants.Value.ToArray();
EndDate.Value = other.EndDate.Value;
if (DateTimeOffset.Now >= EndDate.Value)
Status.Value = new RoomStatusEnded();
// Todo: Temporary, should only remove/add new items (requires framework changes)
if (Playlist.Count == 0)
Playlist.AddRange(other.Playlist);
else if (other.Playlist.Count > 0)
Playlist.First().ID = other.Playlist.First().ID;
foreach (var removedItem in Playlist.Except(other.Playlist).ToArray())
Playlist.Remove(removedItem);
Playlist.AddRange(other.Playlist.Except(Playlist).ToArray());
foreach (var removedItem in Participants.Except(other.Participants).ToArray())
Participants.Remove(removedItem);
Participants.AddRange(other.Participants.Except(Participants).ToArray());
Position = other.Position;
}

View File

@ -0,0 +1,100 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Placeholders;
namespace osu.Game.Online
{
/// <summary>
/// A <see cref="Container"/> for displaying online content which require a local user to be logged in.
/// Shows its children only when the local user is logged in and supports displaying a placeholder if not.
/// </summary>
public abstract class OnlineViewContainer : Container, IOnlineComponent
{
protected LoadingAnimation LoadingAnimation { get; private set; }
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
private readonly string placeholderMessage;
private Placeholder placeholder;
private const double transform_duration = 300;
[Resolved]
protected IAPIProvider API { get; private set; }
protected OnlineViewContainer(string placeholderMessage)
{
this.placeholderMessage = placeholderMessage;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
Content,
placeholder = new LoginPlaceholder(placeholderMessage),
LoadingAnimation = new LoadingAnimation
{
Alpha = 0,
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
API.Register(this);
}
public virtual void APIStateChanged(IAPIProvider api, APIState state)
{
switch (state)
{
case APIState.Offline:
PopContentOut(Content);
placeholder.ScaleTo(0.8f).Then().ScaleTo(1, 3 * transform_duration, Easing.OutQuint);
placeholder.FadeInFromZero(2 * transform_duration, Easing.OutQuint);
LoadingAnimation.Hide();
break;
case APIState.Online:
PopContentIn(Content);
placeholder.FadeOut(transform_duration / 2, Easing.OutQuint);
LoadingAnimation.Hide();
break;
case APIState.Failing:
case APIState.Connecting:
PopContentOut(Content);
LoadingAnimation.Show();
placeholder.FadeOut(transform_duration / 2, Easing.OutQuint);
break;
}
}
/// <summary>
/// Applies a transform to the online content to make it hidden.
/// </summary>
protected virtual void PopContentOut(Drawable content) => content.FadeOut(transform_duration / 2, Easing.OutQuint);
/// <summary>
/// Applies a transform to the online content to make it visible.
/// </summary>
protected virtual void PopContentIn(Drawable content) => content.FadeIn(transform_duration, Easing.OutQuint);
protected override void Dispose(bool isDisposing)
{
API?.Unregister(this);
base.Dispose(isDisposing);
}
}
}

View File

@ -14,7 +14,7 @@ namespace osu.Game.Online.Placeholders
[Resolved(CanBeNull = true)]
private LoginOverlay login { get; set; }
public LoginPlaceholder()
public LoginPlaceholder(string actionMessage)
{
AddIcon(FontAwesome.Solid.UserLock, cp =>
{
@ -22,7 +22,7 @@ namespace osu.Game.Online.Placeholders
cp.Padding = new MarginPadding { Right = 10 };
});
AddText(@"Please sign in to view online leaderboards!");
AddText(actionMessage);
}
protected override bool OnMouseDown(MouseDownEvent e)

View File

@ -401,15 +401,14 @@ namespace osu.Game
if (nextBeatmap?.Track != null)
nextBeatmap.Track.Completed += currentTrackCompleted;
using (var oldBeatmap = beatmap.OldValue)
{
if (oldBeatmap?.Track != null)
oldBeatmap.Track.Completed -= currentTrackCompleted;
}
var oldBeatmap = beatmap.OldValue;
if (oldBeatmap?.Track != null)
oldBeatmap.Track.Completed -= currentTrackCompleted;
updateModDefaults();
nextBeatmap?.LoadBeatmapAsync();
oldBeatmap?.CancelAsyncLoad();
nextBeatmap?.BeginAsyncLoad();
}
private void modsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)

View File

@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Direct
public DownloadProgressBar(BeatmapSetInfo beatmapSet)
: base(beatmapSet)
{
AddInternal(progressBar = new ProgressBar
AddInternal(progressBar = new InteractionDisabledProgressBar
{
Height = 0,
Alpha = 0,
@ -64,5 +64,11 @@ namespace osu.Game.Overlays.Direct
}
}, true);
}
private class InteractionDisabledProgressBar : ProgressBar
{
public override bool HandlePositionalInput => false;
public override bool HandleNonPositionalInput => false;
}
}
}

View File

@ -6,25 +6,14 @@ using osu.Framework.Bindables;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Users;
using System.Collections.Generic;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Allocation;
namespace osu.Game.Overlays.Rankings
{
public class RankingsOverlayHeader : TabControlOverlayHeader<RankingsScope>
{
public readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
public readonly Bindable<Spotlight> Spotlight = new Bindable<Spotlight>();
public readonly Bindable<Country> Country = new Bindable<Country>();
public IEnumerable<Spotlight> Spotlights
{
get => spotlightsContainer.Spotlights;
set => spotlightsContainer.Spotlights = value;
}
protected override ScreenTitle CreateTitle() => new RankingsTitle
{
Scope = { BindTarget = Current }
@ -35,35 +24,11 @@ namespace osu.Game.Overlays.Rankings
Current = Ruleset
};
private SpotlightsContainer spotlightsContainer;
protected override Drawable CreateContent() => new FillFlowContainer
protected override Drawable CreateContent() => new CountryFilter
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new CountryFilter
{
Current = Country
},
spotlightsContainer = new SpotlightsContainer
{
Spotlight = { BindTarget = Spotlight }
}
}
Current = Country
};
protected override void LoadComplete()
{
Current.BindValueChanged(onCurrentChanged, true);
base.LoadComplete();
}
private void onCurrentChanged(ValueChangedEvent<RankingsScope> scope) =>
spotlightsContainer.FadeTo(scope.NewValue == RankingsScope.Spotlights ? 1 : 0, 200, Easing.OutQuint);
private class RankingsTitle : ScreenTitle
{
public readonly Bindable<RankingsScope> Scope = new Bindable<RankingsScope>();
@ -81,48 +46,6 @@ namespace osu.Game.Overlays.Rankings
protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/rankings");
}
private class SpotlightsContainer : CompositeDrawable
{
public readonly Bindable<Spotlight> Spotlight = new Bindable<Spotlight>();
public IEnumerable<Spotlight> Spotlights
{
get => dropdown.Items;
set => dropdown.Items = value;
}
private readonly OsuDropdown<Spotlight> dropdown;
private readonly Box background;
public SpotlightsContainer()
{
Height = 100;
RelativeSizeAxes = Axes.X;
InternalChildren = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
},
dropdown = new OsuDropdown<Spotlight>
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
Width = 0.8f,
Current = Spotlight,
Y = 20,
}
};
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
background.Colour = colourProvider.Dark3;
}
}
}
public enum RankingsScope

View File

@ -1,18 +0,0 @@
// 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 Newtonsoft.Json;
namespace osu.Game.Overlays.Rankings
{
public class Spotlight
{
[JsonProperty("id")]
public int Id;
[JsonProperty("text")]
public string Text;
public override string ToString() => Text;
}
}

View File

@ -14,11 +14,14 @@ using osuTK;
using System;
using System.Collections.Generic;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Online.API.Requests;
namespace osu.Game.Overlays.Rankings
{
public class SpotlightSelector : CompositeDrawable, IHasCurrentValue<APISpotlight>
public class SpotlightSelector : VisibilityContainer, IHasCurrentValue<APISpotlight>
{
private const int duration = 300;
private readonly Box background;
private readonly SpotlightsDropdown dropdown;
@ -36,50 +39,60 @@ namespace osu.Game.Overlays.Rankings
set => dropdown.Items = value;
}
protected override bool StartHidden => true;
private readonly InfoColumn startDateColumn;
private readonly InfoColumn endDateColumn;
private readonly InfoColumn mapCountColumn;
private readonly InfoColumn participantsColumn;
private readonly Container content;
public SpotlightSelector()
{
RelativeSizeAxes = Axes.X;
Height = 100;
InternalChildren = new Drawable[]
Add(content = new Container
{
background = new Box
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, Vertical = 10 },
Children = new Drawable[]
background = new Box
{
dropdown = new SpotlightsDropdown
RelativeSizeAxes = Axes.Both,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, Vertical = 10 },
Children = new Drawable[]
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
Current = Current,
Depth = -float.MaxValue
},
new FillFlowContainer
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(15, 0),
Children = new Drawable[]
dropdown = new SpotlightsDropdown
{
startDateColumn = new InfoColumn(@"Start Date"),
endDateColumn = new InfoColumn(@"End Date"),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
Current = Current,
Depth = -float.MaxValue
},
new FillFlowContainer
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(15, 0),
Children = new Drawable[]
{
startDateColumn = new InfoColumn(@"Start Date"),
endDateColumn = new InfoColumn(@"End Date"),
mapCountColumn = new InfoColumn(@"Map Count"),
participantsColumn = new InfoColumn(@"Participants")
}
}
}
}
},
};
}
});
}
[BackgroundDependencyLoader]
@ -88,18 +101,17 @@ namespace osu.Game.Overlays.Rankings
background.Colour = colourProvider.Dark3;
}
protected override void LoadComplete()
public void ShowInfo(GetSpotlightRankingsResponse response)
{
base.LoadComplete();
Current.BindValueChanged(onCurrentChanged);
startDateColumn.Value = dateToString(response.Spotlight.StartDate);
endDateColumn.Value = dateToString(response.Spotlight.EndDate);
mapCountColumn.Value = response.BeatmapSets.Count.ToString();
participantsColumn.Value = response.Spotlight.Participants?.ToString("N0");
}
private void onCurrentChanged(ValueChangedEvent<APISpotlight> spotlight)
{
startDateColumn.Value = dateToString(spotlight.NewValue.StartDate);
endDateColumn.Value = dateToString(spotlight.NewValue.EndDate);
}
protected override void PopIn() => content.FadeIn(duration, Easing.OutQuint);
protected override void PopOut() => content.FadeOut(duration, Easing.OutQuint);
private string dateToString(DateTimeOffset date) => date.ToString("yyyy-MM-dd");

View File

@ -0,0 +1,161 @@
// 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.Graphics;
using osu.Framework.Bindables;
using osu.Game.Rulesets;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osuTK;
using osu.Framework.Allocation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Rankings.Tables;
using System.Linq;
using osu.Game.Overlays.Direct;
using System.Threading;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Rankings
{
public class SpotlightsLayout : CompositeDrawable
{
public readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
private readonly Bindable<APISpotlight> selectedSpotlight = new Bindable<APISpotlight>();
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private RulesetStore rulesets { get; set; }
private CancellationTokenSource cancellationToken;
private GetSpotlightRankingsRequest getRankingsRequest;
private GetSpotlightsRequest spotlightsRequest;
private SpotlightSelector selector;
private Container content;
private DimmedLoadingLayer loading;
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new ReverseChildIDFillFlowContainer<Drawable>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
selector = new SpotlightSelector
{
Current = selectedSpotlight,
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Vertical = 10 }
},
loading = new DimmedLoadingLayer()
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
selector.Show();
selectedSpotlight.BindValueChanged(onSpotlightChanged);
Ruleset.BindValueChanged(onRulesetChanged);
getSpotlights();
}
private void getSpotlights()
{
spotlightsRequest = new GetSpotlightsRequest();
spotlightsRequest.Success += response => selector.Spotlights = response.Spotlights;
api.Queue(spotlightsRequest);
}
private void onRulesetChanged(ValueChangedEvent<RulesetInfo> ruleset)
{
if (!selector.Spotlights.Any())
return;
selectedSpotlight.TriggerChange();
}
private void onSpotlightChanged(ValueChangedEvent<APISpotlight> spotlight)
{
loading.Show();
cancellationToken?.Cancel();
getRankingsRequest?.Cancel();
getRankingsRequest = new GetSpotlightRankingsRequest(Ruleset.Value, spotlight.NewValue.Id);
getRankingsRequest.Success += onSuccess;
api.Queue(getRankingsRequest);
}
private void onSuccess(GetSpotlightRankingsResponse response)
{
LoadComponentAsync(createContent(response), loaded =>
{
selector.ShowInfo(response);
content.Clear();
content.Add(loaded);
loading.Hide();
}, (cancellationToken = new CancellationTokenSource()).Token);
}
private Drawable createContent(GetSpotlightRankingsResponse response) => new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 20),
Children = new Drawable[]
{
new ScoresTable(1, response.Users),
new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Spacing = new Vector2(10),
Children = response.BeatmapSets.Select(b => new DirectGridPanel(b.ToBeatmapSet(rulesets))
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
}).ToList()
}
}
};
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
spotlightsRequest?.Cancel();
getRankingsRequest?.Cancel();
cancellationToken?.Cancel();
}
}
}

View File

@ -24,7 +24,7 @@ namespace osu.Game.Overlays
private readonly Bindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
private readonly BasicScrollContainer scrollFlow;
private readonly Container tableContainer;
private readonly Container contentContainer;
private readonly DimmedLoadingLayer loading;
private readonly Box background;
@ -69,13 +69,13 @@ namespace osu.Game.Overlays
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
tableContainer = new Container
contentContainer = new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Margin = new MarginPadding { Vertical = 10 }
Margin = new MarginPadding { Bottom = 10 }
},
loading = new DimmedLoadingLayer(),
}
@ -112,7 +112,13 @@ namespace osu.Game.Overlays
Scheduler.AddOnce(loadNewContent);
}, true);
ruleset.BindValueChanged(_ => Scheduler.AddOnce(loadNewContent), true);
ruleset.BindValueChanged(_ =>
{
if (Scope.Value == RankingsScope.Spotlights)
return;
Scheduler.AddOnce(loadNewContent);
}, true);
base.LoadComplete();
}
@ -134,17 +140,26 @@ namespace osu.Game.Overlays
cancellationToken?.Cancel();
lastRequest?.Cancel();
if (Scope.Value == RankingsScope.Spotlights)
{
loadContent(new SpotlightsLayout
{
Ruleset = { BindTarget = ruleset }
});
return;
}
var request = createScopedRequest();
lastRequest = request;
if (request == null)
{
loadTable(null);
loadContent(null);
return;
}
request.Success += () => loadTable(createTableFromResponse(request));
request.Failure += _ => loadTable(null);
request.Success += () => loadContent(createTableFromResponse(request));
request.Failure += _ => loadContent(null);
api.Queue(request);
}
@ -189,21 +204,21 @@ namespace osu.Game.Overlays
return null;
}
private void loadTable(Drawable table)
private void loadContent(Drawable content)
{
scrollFlow.ScrollToStart();
if (table == null)
if (content == null)
{
tableContainer.Clear();
contentContainer.Clear();
loading.Hide();
return;
}
LoadComponentAsync(table, t =>
LoadComponentAsync(content, loaded =>
{
loading.Hide();
tableContainer.Child = table;
contentContainer.Child = loaded;
}, (cancellationToken = new CancellationTokenSource()).Token);
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -20,17 +18,12 @@ namespace osu.Game.Rulesets.Edit
/// <summary>
/// A blueprint which governs the creation of a new <see cref="HitObject"/> to actualisation.
/// </summary>
public abstract class PlacementBlueprint : CompositeDrawable, IStateful<PlacementState>
public abstract class PlacementBlueprint : CompositeDrawable
{
/// <summary>
/// Invoked when <see cref="State"/> has changed.
/// Whether the <see cref="HitObject"/> is currently mid-placement, but has not necessarily finished being placed.
/// </summary>
public event Action<PlacementState> StateChanged;
/// <summary>
/// Whether the <see cref="HitObject"/> is currently being placed, but has not necessarily finished being placed.
/// </summary>
public bool PlacementBegun { get; private set; }
public bool PlacementActive { get; private set; }
/// <summary>
/// The <see cref="HitObject"/> that is being placed.
@ -53,8 +46,6 @@ namespace osu.Game.Rulesets.Edit
// This is required to allow the blueprint's position to be updated via OnMouseMove/Handle
// on the same frame it is made visible via a PlacementState change.
AlwaysPresent = true;
Alpha = 0;
}
[BackgroundDependencyLoader]
@ -67,48 +58,29 @@ namespace osu.Game.Rulesets.Edit
ApplyDefaultsToHitObject();
}
private PlacementState state;
public PlacementState State
{
get => state;
set
{
if (state == value)
return;
state = value;
if (state == PlacementState.Shown)
Show();
else
Hide();
StateChanged?.Invoke(value);
}
}
/// <summary>
/// Signals that the placement of <see cref="HitObject"/> has started.
/// </summary>
/// <param name="startTime">The start time of <see cref="HitObject"/> at the placement point. If null, the current clock time is used.</param>
protected void BeginPlacement(double? startTime = null)
/// <param name="commitStart">Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments.</param>
protected void BeginPlacement(double? startTime = null, bool commitStart = false)
{
HitObject.StartTime = startTime ?? EditorClock.CurrentTime;
placementHandler.BeginPlacement(HitObject);
PlacementBegun = true;
PlacementActive |= commitStart;
}
/// <summary>
/// Signals that the placement of <see cref="HitObject"/> has finished.
/// This will destroy this <see cref="PlacementBlueprint"/>, and add the <see cref="HitObject"/> to the <see cref="Beatmap"/>.
/// This will destroy this <see cref="PlacementBlueprint"/>, and add the HitObject.StartTime to the <see cref="Beatmap"/>.
/// </summary>
/// <param name="commit">Whether the object should be committed.</param>
public void EndPlacement(bool commit)
{
if (!PlacementBegun)
if (!PlacementActive)
BeginPlacement();
placementHandler.EndPlacement(HitObject, commit);
PlacementActive = false;
}
/// <summary>
@ -142,10 +114,4 @@ namespace osu.Game.Rulesets.Edit
}
}
}
public enum PlacementState
{
Hidden,
Shown,
}
}

View File

@ -28,7 +28,11 @@ namespace osu.Game.Rulesets.Mods
public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModHardRock) };
[SettingSource("Drain Rate", "Override a beatmap's set HP.")]
protected const int FIRST_SETTING_ORDER = 1;
protected const int LAST_SETTING_ORDER = 2;
[SettingSource("HP Drain", "Override a beatmap's set HP.", FIRST_SETTING_ORDER)]
public BindableNumber<float> DrainRate { get; } = new BindableFloat
{
Precision = 0.1f,
@ -38,7 +42,7 @@ namespace osu.Game.Rulesets.Mods
Value = 5,
};
[SettingSource("Overall Difficulty", "Override a beatmap's set OD.")]
[SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER)]
public BindableNumber<float> OverallDifficulty { get; } = new BindableFloat
{
Precision = 0.1f,

View File

@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
HitObject.DefaultsApplied += onDefaultsApplied;
startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy();
startTimeBindable.BindValueChanged(_ => updateState(ArmedState.Idle, true));
startTimeBindable.BindValueChanged(_ => updateState(State.Value, true));
if (HitObject is IHasComboInformation combo)
{
@ -250,8 +250,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
double transformTime = HitObject.StartTime - InitialLifetimeOffset;
base.ApplyTransformsAt(transformTime, true);
base.ClearTransformsAfter(transformTime, true);
base.ApplyTransformsAt(double.MinValue, true);
base.ClearTransformsAfter(double.MinValue, true);
using (BeginAbsoluteSequence(transformTime, true))
{

View File

@ -7,7 +7,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects;
@ -62,20 +61,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
private void refreshTool()
{
placementBlueprintContainer.Clear();
currentPlacement?.EndPlacement(false);
currentPlacement = null;
var blueprint = CurrentTool?.CreatePlacementBlueprint();
if (blueprint != null)
{
placementBlueprintContainer.Child = currentPlacement = blueprint;
// Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame
updatePlacementPosition(inputManager.CurrentState.Mouse.Position);
}
removePlacement();
createPlacement();
}
private void updatePlacementPosition(Vector2 screenSpacePosition)
@ -88,33 +75,23 @@ namespace osu.Game.Screens.Edit.Compose.Components
#endregion
protected override bool OnMouseMove(MouseMoveEvent e)
{
if (currentPlacement != null)
{
updatePlacementPosition(e.ScreenSpaceMousePosition);
return true;
}
return base.OnMouseMove(e);
}
protected override void Update()
{
base.Update();
if (composer.CursorInPlacementArea)
createPlacement();
else if (currentPlacement?.PlacementActive == false)
removePlacement();
if (currentPlacement != null)
{
if (composer.CursorInPlacementArea)
currentPlacement.State = PlacementState.Shown;
else if (currentPlacement?.PlacementBegun == false)
currentPlacement.State = PlacementState.Hidden;
}
updatePlacementPosition(inputManager.CurrentState.Mouse.Position);
}
protected sealed override SelectionBlueprint CreateBlueprintFor(HitObject hitObject)
{
var drawable = drawableHitObjects.FirstOrDefault(d => d.HitObject == hitObject);
if (drawable == null)
return null;
@ -129,6 +106,30 @@ namespace osu.Game.Screens.Edit.Compose.Components
base.AddBlueprintFor(hitObject);
}
private void createPlacement()
{
if (currentPlacement != null) return;
var blueprint = CurrentTool?.CreatePlacementBlueprint();
if (blueprint != null)
{
placementBlueprintContainer.Child = currentPlacement = blueprint;
// Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame
updatePlacementPosition(inputManager.CurrentState.Mouse.Position);
}
}
private void removePlacement()
{
if (currentPlacement == null) return;
currentPlacement.EndPlacement(false);
currentPlacement.Expire();
currentPlacement = null;
}
private HitObjectCompositionTool currentTool;
/// <summary>
@ -137,6 +138,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
public HitObjectCompositionTool CurrentTool
{
get => currentTool;
set
{
if (currentTool == value)

View File

@ -6,10 +6,13 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
@ -52,6 +55,45 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
HoverColour = OsuColour.Gray(0.25f);
FlashColour = OsuColour.Gray(0.5f);
}
private ScheduledDelegate repeatSchedule;
/// <summary>
/// The initial delay before mouse down repeat begins.
/// </summary>
private const int repeat_initial_delay = 250;
/// <summary>
/// The delay between mouse down repeats after the initial repeat.
/// </summary>
private const int repeat_tick_rate = 70;
protected override bool OnClick(ClickEvent e)
{
// don't actuate a click since we are manually handling repeats.
return true;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button == MouseButton.Left)
{
Action clickAction = () => base.OnClick(new ClickEvent(e.CurrentState, e.Button));
// run once for initial down
clickAction();
Scheduler.Add(repeatSchedule = new ScheduledDelegate(clickAction, Clock.CurrentTime + repeat_initial_delay, repeat_tick_rate));
}
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
repeatSchedule?.Cancel();
base.OnMouseUp(e);
}
}
}
}

View File

@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers;
using osuTK;
@ -30,6 +32,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private float currentZoom = 1;
[Resolved(canBeNull: true)]
private IFrameBasedClock editorClock { get; set; }
public ZoomableScrollContainer()
: base(Direction.Horizontal)
{
@ -104,8 +109,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override bool OnScroll(ScrollEvent e)
{
if (e.IsPrecise)
{
// can't handle scroll correctly while playing.
// the editor will handle this case for us.
if (editorClock?.IsRunning == true)
return false;
// for now, we don't support zoom when using a precision scroll device. this needs gesture support.
return base.OnScroll(e);
}
setZoomTarget(zoomTarget + e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X);
return true;

View File

@ -95,7 +95,7 @@ namespace osu.Game.Screens.Menu
private void updateAmplitudes()
{
var track = beatmap.Value.TrackLoaded ? beatmap.Value.Track : null;
var effect = beatmap.Value.BeatmapLoaded ? beatmap.Value.Beatmap.ControlPointInfo.EffectPointAt(track?.CurrentTime ?? Time.Current) : null;
var effect = beatmap.Value.BeatmapLoaded ? beatmap.Value.Beatmap?.ControlPointInfo.EffectPointAt(track?.CurrentTime ?? Time.Current) : null;
float[] temporalAmplitudes = track?.CurrentAmplitudes.FrequencyAmplitudes;

View File

@ -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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
@ -25,7 +26,10 @@ namespace osu.Game.Screens.Multi.Components
[BackgroundDependencyLoader]
private void load()
{
CurrentItem.BindValueChanged(_ => updateText(), true);
Playlist.ItemsAdded += _ => updateText();
Playlist.ItemsRemoved += _ => updateText();
updateText();
}
private float textSize = OsuFont.DEFAULT_FONT_SIZE;
@ -54,7 +58,7 @@ namespace osu.Game.Screens.Multi.Components
textFlow.Clear();
var beatmap = CurrentItem.Value?.Beatmap;
var beatmap = Playlist.FirstOrDefault()?.Beatmap;
if (beatmap == null)
{
@ -70,7 +74,7 @@ namespace osu.Game.Screens.Multi.Components
{
new OsuSpriteText
{
Text = new LocalisedString((beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist)),
Text = new LocalisedString((beatmap.Value.Metadata.ArtistUnicode, beatmap.Value.Metadata.Artist)),
Font = OsuFont.GetFont(size: TextSize),
},
new OsuSpriteText
@ -80,10 +84,10 @@ namespace osu.Game.Screens.Multi.Components
},
new OsuSpriteText
{
Text = new LocalisedString((beatmap.Metadata.TitleUnicode, beatmap.Metadata.Title)),
Text = new LocalisedString((beatmap.Value.Metadata.TitleUnicode, beatmap.Value.Metadata.Title)),
Font = OsuFont.GetFont(size: TextSize),
}
}, LinkAction.OpenBeatmap, beatmap.OnlineBeatmapID.ToString(), "Open beatmap");
}, LinkAction.OpenBeatmap, beatmap.Value.OnlineBeatmapID.ToString(), "Open beatmap");
}
}
}

View File

@ -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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -12,6 +13,8 @@ namespace osu.Game.Screens.Multi.Components
{
public class BeatmapTypeInfo : MultiplayerComposite
{
private LinkFlowContainer beatmapAuthor;
public BeatmapTypeInfo()
{
AutoSizeAxes = Axes.Both;
@ -20,8 +23,6 @@ namespace osu.Game.Screens.Multi.Components
[BackgroundDependencyLoader]
private void load()
{
LinkFlowContainer beatmapAuthor;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
@ -50,18 +51,23 @@ namespace osu.Game.Screens.Multi.Components
}
};
CurrentItem.BindValueChanged(item =>
Playlist.ItemsAdded += _ => updateInfo();
Playlist.ItemsRemoved += _ => updateInfo();
updateInfo();
}
private void updateInfo()
{
beatmapAuthor.Clear();
var beatmap = Playlist.FirstOrDefault()?.Beatmap;
if (beatmap != null)
{
beatmapAuthor.Clear();
var beatmap = item.NewValue?.Beatmap;
if (beatmap != null)
{
beatmapAuthor.AddText("mapped by ", s => s.Colour = OsuColour.Gray(0.8f));
beatmapAuthor.AddUserLink(beatmap.Metadata.Author);
}
}, true);
beatmapAuthor.AddText("mapped by ", s => s.Colour = OsuColour.Gray(0.8f));
beatmapAuthor.AddUserLink(beatmap.Value.Metadata.Author);
}
}
}
}

View File

@ -1,11 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Online.Multiplayer;
using osuTK;
namespace osu.Game.Screens.Multi.Components
@ -46,17 +46,22 @@ namespace osu.Game.Screens.Multi.Components
},
};
CurrentItem.BindValueChanged(item => updateBeatmap(item.NewValue), true);
Type.BindValueChanged(type => gameTypeContainer.Child = new DrawableGameType(type.NewValue) { Size = new Vector2(height) }, true);
Playlist.ItemsAdded += _ => updateBeatmap();
Playlist.ItemsRemoved += _ => updateBeatmap();
updateBeatmap();
}
private void updateBeatmap(PlaylistItem item)
private void updateBeatmap()
{
var item = Playlist.FirstOrDefault();
if (item?.Beatmap != null)
{
drawableRuleset.FadeIn(transition_duration);
drawableRuleset.Child = new DifficultyIcon(item.Beatmap, item.Ruleset) { Size = new Vector2(height) };
drawableRuleset.Child = new DifficultyIcon(item.Beatmap.Value, item.Ruleset.Value) { Size = new Vector2(height) };
}
else
drawableRuleset.FadeOut(transition_duration);

View File

@ -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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.Drawables;
@ -10,6 +11,7 @@ namespace osu.Game.Screens.Multi.Components
public class MultiplayerBackgroundSprite : MultiplayerComposite
{
private readonly BeatmapSetCoverType beatmapSetCoverType;
private UpdateableBeatmapBackgroundSprite sprite;
public MultiplayerBackgroundSprite(BeatmapSetCoverType beatmapSetCoverType = BeatmapSetCoverType.Cover)
{
@ -19,11 +21,17 @@ namespace osu.Game.Screens.Multi.Components
[BackgroundDependencyLoader]
private void load()
{
UpdateableBeatmapBackgroundSprite sprite;
InternalChild = sprite = CreateBackgroundSprite();
CurrentItem.BindValueChanged(item => sprite.Beatmap.Value = item.NewValue?.Beatmap, true);
Playlist.ItemsAdded += _ => updateBeatmap();
Playlist.ItemsRemoved += _ => updateBeatmap();
updateBeatmap();
}
private void updateBeatmap()
{
sprite.Beatmap.Value = Playlist.FirstOrDefault()?.Beatmap.Value;
}
protected virtual UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new UpdateableBeatmapBackgroundSprite(beatmapSetCoverType) { RelativeSizeAxes = Axes.Both };

View File

@ -68,7 +68,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components
{
bool matchingFilter = true;
matchingFilter &= r.Room.Playlist.Count == 0 || r.Room.Playlist.Any(i => i.Ruleset.Equals(criteria.Ruleset));
matchingFilter &= r.Room.Playlist.Count == 0 || r.Room.Playlist.Any(i => i.Ruleset.Value.Equals(criteria.Ruleset));
if (!string.IsNullOrEmpty(criteria.SearchString))
matchingFilter &= r.FilterTerms.Any(term => term.IndexOf(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase) >= 0);

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@ -32,6 +33,7 @@ namespace osu.Game.Screens.Multi.Match.Components
public Action RequestBeatmapSelection;
private MatchBeatmapPanel beatmapPanel;
private ModDisplay modDisplay;
public Header()
{
@ -43,7 +45,6 @@ namespace osu.Game.Screens.Multi.Match.Components
private void load(OsuColour colours)
{
BeatmapSelectButton beatmapButton;
ModDisplay modDisplay;
InternalChildren = new Drawable[]
{
@ -119,9 +120,12 @@ namespace osu.Game.Screens.Multi.Match.Components
},
};
CurrentItem.BindValueChanged(item => modDisplay.Current.Value = item.NewValue?.RequiredMods?.ToArray() ?? Array.Empty<Mod>(), true);
beatmapButton.Action = () => RequestBeatmapSelection?.Invoke();
Playlist.ItemsAdded += _ => updateMods();
Playlist.ItemsRemoved += _ => updateMods();
updateMods();
}
protected override void LoadComplete()
@ -130,6 +134,13 @@ namespace osu.Game.Screens.Multi.Match.Components
ShowBeatmapPanel.BindValueChanged(value => beatmapPanel.FadeTo(value.NewValue ? 1 : 0, 200, Easing.OutQuint), true);
}
private void updateMods()
{
var item = Playlist.FirstOrDefault();
modDisplay.Current.Value = item?.RequiredMods?.ToArray() ?? Array.Empty<Mod>();
}
private class BeatmapSelectButton : HeaderButton
{
[Resolved(typeof(Room), nameof(Room.RoomID))]

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -18,6 +19,8 @@ namespace osu.Game.Screens.Multi.Match.Components
{
public Action OnStart;
private ReadyButton readyButton;
public Info()
{
RelativeSizeAxes = Axes.X;
@ -27,7 +30,6 @@ namespace osu.Game.Screens.Multi.Match.Components
[BackgroundDependencyLoader]
private void load()
{
ReadyButton readyButton;
HostInfo hostInfo;
InternalChildren = new Drawable[]
@ -89,9 +91,17 @@ namespace osu.Game.Screens.Multi.Match.Components
},
};
CurrentItem.BindValueChanged(item => readyButton.Beatmap.Value = item.NewValue?.Beatmap, true);
hostInfo.Host.BindTo(Host);
Playlist.ItemsAdded += _ => updateBeatmap();
Playlist.ItemsRemoved += _ => updateBeatmap();
updateBeatmap();
}
private void updateBeatmap()
{
readyButton.Beatmap.Value = Playlist.FirstOrDefault()?.Beatmap.Value;
}
}
}

View File

@ -1,10 +1,10 @@
// 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 System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Direct;
@ -32,10 +32,13 @@ namespace osu.Game.Screens.Multi.Match.Components
[BackgroundDependencyLoader]
private void load()
{
CurrentItem.BindValueChanged(item => loadNewPanel(item.NewValue?.Beatmap), true);
Playlist.ItemsAdded += _ => loadNewPanel();
Playlist.ItemsRemoved += _ => loadNewPanel();
loadNewPanel();
}
private void loadNewPanel(BeatmapInfo beatmap)
private void loadNewPanel()
{
loadCancellation?.Cancel();
request?.Cancel();
@ -44,6 +47,8 @@ namespace osu.Game.Screens.Multi.Match.Components
panel?.Expire();
panel = null;
var beatmap = Playlist.FirstOrDefault()?.Beatmap.Value;
if (beatmap?.OnlineBeatmapID == null)
return;

View File

@ -51,9 +51,9 @@ namespace osu.Game.Screens.Multi.Match.Components
},
};
Participants.BindValueChanged(participants =>
Participants.ItemsAdded += users =>
{
usersFlow.Children = participants.NewValue.Select(u =>
usersFlow.AddRange(users.Select(u =>
{
var panel = new UserPanel(u)
{
@ -65,8 +65,13 @@ namespace osu.Game.Screens.Multi.Match.Components
panel.OnLoadComplete += d => d.FadeInFromZero(60);
return panel;
}).ToList();
}, true);
}).ToList());
};
Participants.ItemsRemoved += users =>
{
usersFlow.RemoveAll(p => users.Contains(p.User));
};
}
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -40,9 +41,6 @@ namespace osu.Game.Screens.Multi.Match
[Resolved(typeof(Room))]
protected BindableList<PlaylistItem> Playlist { get; private set; }
[Resolved(typeof(Room))]
protected Bindable<PlaylistItem> CurrentItem { get; private set; }
[Resolved]
private BeatmapManager beatmapManager { get; set; }
@ -52,6 +50,7 @@ namespace osu.Game.Screens.Multi.Match
[Resolved(CanBeNull = true)]
private OsuGame game { get; set; }
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
private MatchLeaderboard leaderboard;
public MatchSubScreen(Room room)
@ -165,7 +164,16 @@ namespace osu.Game.Screens.Multi.Match
{
base.LoadComplete();
CurrentItem.BindValueChanged(currentItemChanged, true);
Playlist.ItemsAdded += _ => updateSelectedItem();
Playlist.ItemsRemoved += _ => updateSelectedItem();
updateSelectedItem();
}
private void updateSelectedItem()
{
selectedItem.Value = Playlist.FirstOrDefault();
currentItemChanged();
}
public override bool OnExiting(IScreen next)
@ -180,16 +188,18 @@ namespace osu.Game.Screens.Multi.Match
/// <summary>
/// Handles propagation of the current playlist item's content to game-wide mechanisms.
/// </summary>
private void currentItemChanged(ValueChangedEvent<PlaylistItem> e)
private void currentItemChanged()
{
var item = selectedItem.Value;
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
var localBeatmap = e.NewValue?.Beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == e.NewValue.Beatmap.OnlineBeatmapID);
var localBeatmap = item?.Beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == item.Beatmap.Value.OnlineBeatmapID);
Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
Mods.Value = e.NewValue?.RequiredMods?.ToArray() ?? Array.Empty<Mod>();
Mods.Value = item?.RequiredMods?.ToArray() ?? Array.Empty<Mod>();
if (e.NewValue?.Ruleset != null)
Ruleset.Value = e.NewValue.Ruleset;
if (item?.Ruleset != null)
Ruleset.Value = item.Ruleset.Value;
previewTrackManager.StopAnyPlaying(this);
}
@ -202,11 +212,11 @@ namespace osu.Game.Screens.Multi.Match
if (Beatmap.Value != beatmapManager.DefaultBeatmap)
return;
if (CurrentItem.Value == null)
if (selectedItem.Value == null)
return;
// Try to retrieve the corresponding local beatmap
var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == CurrentItem.Value.Beatmap.OnlineBeatmapID);
var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == selectedItem.Value.Beatmap.Value.OnlineBeatmapID);
if (localBeatmap != null)
Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
@ -223,7 +233,7 @@ namespace osu.Game.Screens.Multi.Match
{
default:
case GameTypeTimeshift _:
multiplayer?.Start(() => new TimeshiftPlayer(CurrentItem.Value)
multiplayer?.Start(() => new TimeshiftPlayer(selectedItem.Value)
{
Exited = () => leaderboard.RefreshScores()
});

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
@ -32,10 +31,7 @@ namespace osu.Game.Screens.Multi
protected BindableList<PlaylistItem> Playlist { get; private set; }
[Resolved(typeof(Room))]
protected Bindable<PlaylistItem> CurrentItem { get; private set; }
[Resolved(typeof(Room))]
protected Bindable<IEnumerable<User>> Participants { get; private set; }
protected BindableList<User> Participants { get; private set; }
[Resolved(typeof(Room))]
protected Bindable<int> ParticipantCount { get; private set; }

View File

@ -50,10 +50,10 @@ namespace osu.Game.Screens.Multi.Play
bool failed = false;
// Sanity checks to ensure that TimeshiftPlayer matches the settings for the current PlaylistItem
if (Beatmap.Value.BeatmapInfo.OnlineBeatmapID != playlistItem.Beatmap.OnlineBeatmapID)
if (Beatmap.Value.BeatmapInfo.OnlineBeatmapID != playlistItem.Beatmap.Value.OnlineBeatmapID)
throw new InvalidOperationException("Current Beatmap does not match PlaylistItem's Beatmap");
if (ruleset.Value.ID != playlistItem.Ruleset.ID)
if (ruleset.Value.ID != playlistItem.Ruleset.Value.ID)
throw new InvalidOperationException("Current Ruleset does not match PlaylistItem's Ruleset");
if (!playlistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals)))

View File

@ -0,0 +1,180 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play.HUD;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Play
{
/// <summary>
/// Displays beatmap metadata inside <see cref="PlayerLoader"/>
/// </summary>
public class BeatmapMetadataDisplay : Container
{
private class MetadataLine : Container
{
public MetadataLine(string left, string right)
{
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopRight,
Margin = new MarginPadding { Right = 5 },
Colour = OsuColour.Gray(0.8f),
Text = left,
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopLeft,
Margin = new MarginPadding { Left = 5 },
Text = string.IsNullOrEmpty(right) ? @"-" : right,
}
};
}
}
private readonly WorkingBeatmap beatmap;
private readonly Bindable<IReadOnlyList<Mod>> mods;
private readonly Drawable facade;
private LoadingAnimation loading;
private Sprite backgroundSprite;
public IBindable<IReadOnlyList<Mod>> Mods => mods;
public bool Loading
{
set
{
if (value)
{
loading.Show();
backgroundSprite.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint);
}
else
{
loading.Hide();
backgroundSprite.FadeColour(Color4.White, 400, Easing.OutQuint);
}
}
}
public BeatmapMetadataDisplay(WorkingBeatmap beatmap, Bindable<IReadOnlyList<Mod>> mods, Drawable facade)
{
this.beatmap = beatmap;
this.facade = facade;
this.mods = new Bindable<IReadOnlyList<Mod>>();
this.mods.BindTo(mods);
}
[BackgroundDependencyLoader]
private void load()
{
var metadata = beatmap.BeatmapInfo?.Metadata ?? new BeatmapMetadata();
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Direction = FillDirection.Vertical,
Children = new[]
{
facade.With(d =>
{
d.Anchor = Anchor.TopCentre;
d.Origin = Anchor.TopCentre;
}),
new OsuSpriteText
{
Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)),
Font = OsuFont.GetFont(size: 36, italics: true),
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Margin = new MarginPadding { Top = 15 },
},
new OsuSpriteText
{
Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)),
Font = OsuFont.GetFont(size: 26, italics: true),
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
},
new Container
{
Size = new Vector2(300, 60),
Margin = new MarginPadding(10),
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
CornerRadius = 10,
Masking = true,
Children = new Drawable[]
{
backgroundSprite = new Sprite
{
RelativeSizeAxes = Axes.Both,
Texture = beatmap?.Background,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
FillMode = FillMode.Fill,
},
loading = new LoadingAnimation { Scale = new Vector2(1.3f) }
}
},
new OsuSpriteText
{
Text = beatmap?.BeatmapInfo?.Version,
Font = OsuFont.GetFont(size: 26, italics: true),
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Margin = new MarginPadding
{
Bottom = 40
},
},
new MetadataLine("Source", metadata.Source)
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
},
new MetadataLine("Mapper", metadata.AuthorString)
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
},
new ModDisplay
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Top = 20 },
Current = mods
}
},
}
};
Loading = true;
}
}
}

View File

@ -0,0 +1,42 @@
// 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 osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Play
{
public class GameplayBeatmap : Component, IBeatmap
{
public readonly IBeatmap PlayableBeatmap;
public GameplayBeatmap(IBeatmap playableBeatmap)
{
PlayableBeatmap = playableBeatmap;
}
public BeatmapInfo BeatmapInfo
{
get => PlayableBeatmap.BeatmapInfo;
set => PlayableBeatmap.BeatmapInfo = value;
}
public BeatmapMetadata Metadata => PlayableBeatmap.Metadata;
public ControlPointInfo ControlPointInfo => PlayableBeatmap.ControlPointInfo;
public List<BreakPeriod> Breaks => PlayableBeatmap.Breaks;
public double TotalBreakTime => PlayableBeatmap.TotalBreakTime;
public IReadOnlyList<HitObject> HitObjects => PlayableBeatmap.HitObjects;
public IEnumerable<BeatmapStatistic> GetStatistics() => PlayableBeatmap.GetStatistics();
public IBeatmap Clone() => PlayableBeatmap.Clone();
}
}

View File

@ -24,7 +24,7 @@ namespace osu.Game.Screens.Play.HUD
public bool DisplayUnrankedText = true;
public bool AllowExpand = true;
public ExpansionMode ExpansionMode = ExpansionMode.ExpandOnHover;
private readonly Bindable<IReadOnlyList<Mod>> current = new Bindable<IReadOnlyList<Mod>>();
@ -110,11 +110,15 @@ namespace osu.Game.Screens.Play.HUD
private void expand()
{
if (AllowExpand)
if (ExpansionMode != ExpansionMode.AlwaysContracted)
IconsContainer.TransformSpacingTo(new Vector2(5, 0), 500, Easing.OutQuint);
}
private void contract() => IconsContainer.TransformSpacingTo(new Vector2(-25, 0), 500, Easing.OutQuint);
private void contract()
{
if (ExpansionMode != ExpansionMode.AlwaysExpanded)
IconsContainer.TransformSpacingTo(new Vector2(-25, 0), 500, Easing.OutQuint);
}
protected override bool OnHover(HoverEvent e)
{
@ -128,4 +132,22 @@ namespace osu.Game.Screens.Play.HUD
base.OnHoverLost(e);
}
}
public enum ExpansionMode
{
/// <summary>
/// The <see cref="ModDisplay"/> will expand only when hovered.
/// </summary>
ExpandOnHover,
/// <summary>
/// The <see cref="ModDisplay"/> will always be expanded.
/// </summary>
AlwaysExpanded,
/// <summary>
/// The <see cref="ModDisplay"/> will always be contracted.
/// </summary>
AlwaysContracted
}
}

View File

@ -110,6 +110,13 @@ namespace osu.Game.Screens.Play
this.showResults = showResults;
}
private GameplayBeatmap gameplayBeatmap;
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
[BackgroundDependencyLoader]
private void load(AudioManager audio, IAPIProvider api, OsuConfigManager config)
{
@ -143,6 +150,10 @@ namespace osu.Game.Screens.Play
InternalChild = GameplayClockContainer = new GameplayClockContainer(Beatmap.Value, Mods.Value, DrawableRuleset.GameplayStartTime);
AddInternal(gameplayBeatmap = new GameplayBeatmap(playableBeatmap));
dependencies.CacheAs(gameplayBeatmap);
addUnderlayComponents(GameplayClockContainer);
addGameplayComponents(GameplayClockContainer, Beatmap.Value);
addOverlayComponents(GameplayClockContainer, Beatmap.Value);

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
@ -12,21 +11,15 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Localisation;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Users;
using osuTK;
@ -38,30 +31,58 @@ namespace osu.Game.Screens.Play
{
protected const float BACKGROUND_BLUR = 15;
public override bool HideOverlaysOnEnter => hideOverlays;
public override bool DisallowExternalBeatmapRulesetChanges => true;
// Here because IsHovered will not update unless we do so.
public override bool HandlePositionalInput => true;
// We show the previous screen status
protected override UserActivity InitialActivity => null;
protected override bool PlayResumeSound => false;
protected BeatmapMetadataDisplay MetadataInfo;
protected VisualSettings VisualSettings;
protected Task LoadTask { get; private set; }
protected Task DisposalTask { get; private set; }
private bool backgroundBrightnessReduction;
protected bool BackgroundBrightnessReduction
{
set
{
if (value == backgroundBrightnessReduction)
return;
backgroundBrightnessReduction = value;
Background.FadeColour(OsuColour.Gray(backgroundBrightnessReduction ? 0.8f : 1), 200);
}
}
private bool readyForPush =>
player.LoadState == LoadState.Ready && (IsHovered || idleTracker.IsIdle.Value) && inputManager?.DraggedDrawable == null;
private readonly Func<Player> createPlayer;
private Player player;
private LogoTrackingContainer content;
protected BeatmapMetadataDisplay MetadataInfo;
private bool hideOverlays;
public override bool HideOverlaysOnEnter => hideOverlays;
protected override UserActivity InitialActivity => null; //shows the previous screen status
public override bool DisallowExternalBeatmapRulesetChanges => true;
protected override bool PlayResumeSound => false;
protected Task LoadTask { get; private set; }
protected Task DisposalTask { get; private set; }
private InputManager inputManager;
private IdleTracker idleTracker;
private ScheduledDelegate scheduledPushPlayer;
[Resolved(CanBeNull = true)]
private NotificationOverlay notificationOverlay { get; set; }
@ -71,19 +92,11 @@ namespace osu.Game.Screens.Play
[Resolved]
private AudioManager audioManager { get; set; }
private Bindable<bool> muteWarningShownOnce;
public PlayerLoader(Func<Player> createPlayer)
{
this.createPlayer = createPlayer;
}
private void restartRequested()
{
hideOverlays = true;
ValidForResume = true;
}
[BackgroundDependencyLoader]
private void load(SessionStatics sessionStatics)
{
@ -127,11 +140,13 @@ namespace osu.Game.Screens.Play
inputManager = GetContainingInputManager();
}
#region Screen handling
public override void OnEntering(IScreen last)
{
base.OnEntering(last);
loadNewPlayer();
prepareNewPlayer();
content.ScaleTo(0.7f);
Background?.FadeColour(Color4.White, 800, Easing.OutQuint);
@ -141,15 +156,7 @@ namespace osu.Game.Screens.Play
MetadataInfo.Delay(750).FadeIn(500);
this.Delay(1800).Schedule(pushWhenLoaded);
if (!muteWarningShownOnce.Value)
{
//Checks if the notification has not been shown yet and also if master volume is muted, track/music volume is muted or if the whole game is muted.
if (volumeOverlay?.IsMuted.Value == true || audioManager.Volume.Value <= audioManager.Volume.MinValue || audioManager.VolumeTrack.Value <= audioManager.VolumeTrack.MinValue)
{
notificationOverlay?.Post(new MutedNotification());
muteWarningShownOnce.Value = true;
}
}
showMuteWarningIfNeeded();
}
public override void OnResuming(IScreen last)
@ -160,36 +167,32 @@ namespace osu.Game.Screens.Play
MetadataInfo.Loading = true;
//we will only be resumed if the player has requested a re-run (see ValidForResume setting above)
loadNewPlayer();
// we will only be resumed if the player has requested a re-run (see restartRequested).
prepareNewPlayer();
this.Delay(400).Schedule(pushWhenLoaded);
}
private void loadNewPlayer()
public override void OnSuspending(IScreen next)
{
var restartCount = player?.RestartCount + 1 ?? 0;
base.OnSuspending(next);
player = createPlayer();
player.RestartCount = restartCount;
player.RestartRequested = restartRequested;
cancelLoad();
LoadTask = LoadComponentAsync(player, _ => MetadataInfo.Loading = false);
BackgroundBrightnessReduction = false;
}
private void contentIn()
public override bool OnExiting(IScreen next)
{
content.ScaleTo(1, 650, Easing.OutQuint);
content.FadeInFromZero(400);
}
cancelLoad();
private void contentOut()
{
// Ensure the logo is no longer tracking before we scale the content
content.StopTracking();
content.ScaleTo(0.7f, 150, Easing.InQuint);
this.FadeOut(150);
content.ScaleTo(0.7f, 300, Easing.InQuint);
content.FadeOut(250);
Background.EnableUserDim.Value = false;
BackgroundBrightnessReduction = false;
return base.OnExiting(next);
}
protected override void LogoArriving(OsuLogo logo, bool resuming)
@ -198,10 +201,7 @@ namespace osu.Game.Screens.Play
const double duration = 300;
if (!resuming)
{
logo.MoveTo(new Vector2(0.5f), duration, Easing.In);
}
if (!resuming) logo.MoveTo(new Vector2(0.5f), duration, Easing.In);
logo.ScaleTo(new Vector2(0.15f), duration, Easing.In);
logo.FadeIn(350);
@ -219,109 +219,7 @@ namespace osu.Game.Screens.Play
content.StopTracking();
}
private ScheduledDelegate pushDebounce;
protected VisualSettings VisualSettings;
// Here because IsHovered will not update unless we do so.
public override bool HandlePositionalInput => true;
private bool readyForPush => player.LoadState == LoadState.Ready && (IsHovered || idleTracker.IsIdle.Value) && inputManager?.DraggedDrawable == null;
private void pushWhenLoaded()
{
if (!this.IsCurrentScreen()) return;
try
{
if (!readyForPush)
{
// as the pushDebounce below has a delay, we need to keep checking and cancel a future debounce
// if we become unready for push during the delay.
cancelLoad();
return;
}
if (pushDebounce != null)
return;
pushDebounce = Scheduler.AddDelayed(() =>
{
contentOut();
this.Delay(250).Schedule(() =>
{
if (!this.IsCurrentScreen()) return;
LoadTask = null;
//By default, we want to load the player and never be returned to.
//Note that this may change if the player we load requested a re-run.
ValidForResume = false;
if (player.LoadedBeatmapSuccessfully)
this.Push(player);
else
this.Exit();
});
}, 500);
}
finally
{
Schedule(pushWhenLoaded);
}
}
private void cancelLoad()
{
pushDebounce?.Cancel();
pushDebounce = null;
}
public override void OnSuspending(IScreen next)
{
BackgroundBrightnessReduction = false;
base.OnSuspending(next);
cancelLoad();
}
public override bool OnExiting(IScreen next)
{
content.ScaleTo(0.7f, 150, Easing.InQuint);
this.FadeOut(150);
cancelLoad();
Background.EnableUserDim.Value = false;
BackgroundBrightnessReduction = false;
return base.OnExiting(next);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (isDisposing)
{
// if the player never got pushed, we should explicitly dispose it.
DisposalTask = LoadTask?.ContinueWith(_ => player.Dispose());
}
}
private bool backgroundBrightnessReduction;
protected bool BackgroundBrightnessReduction
{
get => backgroundBrightnessReduction;
set
{
if (value == backgroundBrightnessReduction)
return;
backgroundBrightnessReduction = value;
Background.FadeColour(OsuColour.Gray(backgroundBrightnessReduction ? 0.8f : 1), 200);
}
}
#endregion
protected override void Update()
{
@ -350,171 +248,129 @@ namespace osu.Game.Screens.Play
}
}
protected class BeatmapMetadataDisplay : Container
private void prepareNewPlayer()
{
private class MetadataLine : Container
var restartCount = player?.RestartCount + 1 ?? 0;
player = createPlayer();
player.RestartCount = restartCount;
player.RestartRequested = restartRequested;
LoadTask = LoadComponentAsync(player, _ => MetadataInfo.Loading = false);
}
private void restartRequested()
{
hideOverlays = true;
ValidForResume = true;
}
private void contentIn()
{
content.ScaleTo(1, 650, Easing.OutQuint);
content.FadeInFromZero(400);
}
private void contentOut()
{
// Ensure the logo is no longer tracking before we scale the content
content.StopTracking();
content.ScaleTo(0.7f, 300, Easing.InQuint);
content.FadeOut(250);
}
private void pushWhenLoaded()
{
if (!this.IsCurrentScreen()) return;
try
{
public MetadataLine(string left, string right)
if (!readyForPush)
{
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopRight,
Margin = new MarginPadding { Right = 5 },
Colour = OsuColour.Gray(0.8f),
Text = left,
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopLeft,
Margin = new MarginPadding { Left = 5 },
Text = string.IsNullOrEmpty(right) ? @"-" : right,
}
};
// as the pushDebounce below has a delay, we need to keep checking and cancel a future debounce
// if we become unready for push during the delay.
cancelLoad();
return;
}
}
private readonly WorkingBeatmap beatmap;
private readonly Bindable<IReadOnlyList<Mod>> mods;
private readonly Drawable facade;
private LoadingAnimation loading;
private Sprite backgroundSprite;
if (scheduledPushPlayer != null)
return;
public IBindable<IReadOnlyList<Mod>> Mods => mods;
public bool Loading
{
set
scheduledPushPlayer = Scheduler.AddDelayed(() =>
{
if (value)
contentOut();
this.Delay(250).Schedule(() =>
{
loading.Show();
backgroundSprite.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint);
}
else
{
loading.Hide();
backgroundSprite.FadeColour(Color4.White, 400, Easing.OutQuint);
}
if (!this.IsCurrentScreen()) return;
LoadTask = null;
//By default, we want to load the player and never be returned to.
//Note that this may change if the player we load requested a re-run.
ValidForResume = false;
if (player.LoadedBeatmapSuccessfully)
this.Push(player);
else
this.Exit();
});
}, 500);
}
finally
{
Schedule(pushWhenLoaded);
}
}
private void cancelLoad()
{
scheduledPushPlayer?.Cancel();
scheduledPushPlayer = null;
}
#region Disposal
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (isDisposing)
{
// if the player never got pushed, we should explicitly dispose it.
DisposalTask = LoadTask?.ContinueWith(_ => player.Dispose());
}
}
#endregion
#region Mute warning
private Bindable<bool> muteWarningShownOnce;
private void showMuteWarningIfNeeded()
{
if (!muteWarningShownOnce.Value)
{
//Checks if the notification has not been shown yet and also if master volume is muted, track/music volume is muted or if the whole game is muted.
if (volumeOverlay?.IsMuted.Value == true || audioManager.Volume.Value <= audioManager.Volume.MinValue || audioManager.VolumeTrack.Value <= audioManager.VolumeTrack.MinValue)
{
notificationOverlay?.Post(new MutedNotification());
muteWarningShownOnce.Value = true;
}
}
public BeatmapMetadataDisplay(WorkingBeatmap beatmap, Bindable<IReadOnlyList<Mod>> mods, Drawable facade)
{
this.beatmap = beatmap;
this.facade = facade;
this.mods = new Bindable<IReadOnlyList<Mod>>();
this.mods.BindTo(mods);
}
[BackgroundDependencyLoader]
private void load()
{
var metadata = beatmap.BeatmapInfo?.Metadata ?? new BeatmapMetadata();
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Direction = FillDirection.Vertical,
Children = new[]
{
facade.With(d =>
{
d.Anchor = Anchor.TopCentre;
d.Origin = Anchor.TopCentre;
}),
new OsuSpriteText
{
Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)),
Font = OsuFont.GetFont(size: 36, italics: true),
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Margin = new MarginPadding { Top = 15 },
},
new OsuSpriteText
{
Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)),
Font = OsuFont.GetFont(size: 26, italics: true),
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
},
new Container
{
Size = new Vector2(300, 60),
Margin = new MarginPadding(10),
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
CornerRadius = 10,
Masking = true,
Children = new Drawable[]
{
backgroundSprite = new Sprite
{
RelativeSizeAxes = Axes.Both,
Texture = beatmap?.Background,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
FillMode = FillMode.Fill,
},
loading = new LoadingAnimation { Scale = new Vector2(1.3f) }
}
},
new OsuSpriteText
{
Text = beatmap?.BeatmapInfo?.Version,
Font = OsuFont.GetFont(size: 26, italics: true),
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Margin = new MarginPadding
{
Bottom = 40
},
},
new MetadataLine("Source", metadata.Source)
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
},
new MetadataLine("Mapper", metadata.AuthorString)
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
},
new ModDisplay
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Top = 20 },
Current = mods
}
},
}
};
Loading = true;
}
}
private class MutedNotification : SimpleNotification
{
public override bool IsImportant => true;
public MutedNotification()
{
Text = "Your music volume is set to 0%! Click here to restore it.";
}
public override bool IsImportant => true;
[BackgroundDependencyLoader]
private void load(OsuColour colours, AudioManager audioManager, NotificationOverlay notificationOverlay, VolumeOverlay volumeOverlay)
{
@ -533,5 +389,7 @@ namespace osu.Game.Screens.Play
};
}
}
#endregion
}
}

View File

@ -2,37 +2,40 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select.Leaderboards;
namespace osu.Game.Screens.Select
{
public class BeatmapDetailArea : Container
public abstract class BeatmapDetailArea : Container
{
private const float details_padding = 10;
private readonly Container content;
protected override Container<Drawable> Content => content;
public readonly BeatmapDetails Details;
public readonly BeatmapLeaderboard Leaderboard;
private WorkingBeatmap beatmap;
public WorkingBeatmap Beatmap
public virtual WorkingBeatmap Beatmap
{
get => beatmap;
set
{
beatmap = value;
Details.Beatmap = beatmap?.BeatmapInfo;
Leaderboard.Beatmap = beatmap is DummyWorkingBeatmap ? null : beatmap?.BeatmapInfo;
Details.Beatmap = value?.BeatmapInfo;
}
}
public BeatmapDetailArea()
public readonly BeatmapDetails Details;
protected Bindable<BeatmapDetailAreaTabItem> CurrentTab => tabControl.Current;
private readonly Container content;
protected override Container<Drawable> Content => content;
private readonly BeatmapDetailAreaTabControl tabControl;
protected BeatmapDetailArea()
{
AddRangeInternal(new Drawable[]
{
@ -40,51 +43,62 @@ namespace osu.Game.Screens.Select
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = BeatmapDetailAreaTabControl.HEIGHT },
},
new BeatmapDetailAreaTabControl
{
RelativeSizeAxes = Axes.X,
OnFilter = (tab, mods) =>
Child = Details = new BeatmapDetails
{
Leaderboard.FilterMods = mods;
switch (tab)
{
case BeatmapDetailTab.Details:
Details.Show();
Leaderboard.Hide();
break;
default:
Details.Hide();
Leaderboard.Scope = (BeatmapLeaderboardScope)tab - 1;
Leaderboard.Show();
break;
}
},
RelativeSizeAxes = Axes.X,
Alpha = 0,
Margin = new MarginPadding { Top = details_padding },
}
},
});
AddRange(new Drawable[]
{
Details = new BeatmapDetails
tabControl = new BeatmapDetailAreaTabControl
{
RelativeSizeAxes = Axes.X,
Alpha = 0,
Margin = new MarginPadding { Top = details_padding },
TabItems = CreateTabItems(),
OnFilter = OnTabChanged,
},
Leaderboard = new BeatmapLeaderboard
{
RelativeSizeAxes = Axes.Both,
}
});
}
/// <summary>
/// Refreshes the currently-displayed details.
/// </summary>
public virtual void Refresh()
{
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
Details.Height = Math.Min(DrawHeight - details_padding * 3 - BeatmapDetailAreaTabControl.HEIGHT, 450);
}
/// <summary>
/// Invoked when a new tab is selected.
/// </summary>
/// <param name="tab">The tab that was selected.</param>
/// <param name="selectedMods">Whether the currently-selected mods should be considered.</param>
protected virtual void OnTabChanged(BeatmapDetailAreaTabItem tab, bool selectedMods)
{
switch (tab)
{
case BeatmapDetailAreaDetailTabItem _:
Details.Show();
break;
default:
Details.Hide();
break;
}
}
/// <summary>
/// Creates the tabs to be displayed.
/// </summary>
/// <returns>The tabs.</returns>
protected virtual BeatmapDetailAreaTabItem[] CreateTabItems() => new BeatmapDetailAreaTabItem[]
{
new BeatmapDetailAreaDetailTabItem(),
};
}
}

View File

@ -0,0 +1,10 @@
// 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.
namespace osu.Game.Screens.Select
{
public class BeatmapDetailAreaDetailTabItem : BeatmapDetailAreaTabItem
{
public override string Name => "Details";
}
}

View File

@ -0,0 +1,22 @@
// 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;
namespace osu.Game.Screens.Select
{
public class BeatmapDetailAreaLeaderboardTabItem<TScope> : BeatmapDetailAreaTabItem
where TScope : Enum
{
public override string Name => Scope.ToString();
public override bool FilterableByMods => true;
public readonly TScope Scope;
public BeatmapDetailAreaLeaderboardTabItem(TScope scope)
{
Scope = scope;
}
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -18,14 +19,25 @@ namespace osu.Game.Screens.Select
public class BeatmapDetailAreaTabControl : Container
{
public const float HEIGHT = 24;
public Bindable<BeatmapDetailAreaTabItem> Current
{
get => tabs.Current;
set => tabs.Current = value;
}
public Action<BeatmapDetailAreaTabItem, bool> OnFilter; //passed the selected tab and if mods is checked
public IReadOnlyList<BeatmapDetailAreaTabItem> TabItems
{
get => tabs.Items;
set => tabs.Items = value;
}
private readonly OsuTabControlCheckbox modsCheckbox;
private readonly OsuTabControl<BeatmapDetailTab> tabs;
private readonly OsuTabControl<BeatmapDetailAreaTabItem> tabs;
private readonly Container tabsContainer;
public Action<BeatmapDetailTab, bool> OnFilter; //passed the selected tab and if mods is checked
private Bindable<BeatmapDetailTab> selectedTab;
public BeatmapDetailAreaTabControl()
{
Height = HEIGHT;
@ -43,7 +55,7 @@ namespace osu.Game.Screens.Select
tabsContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Child = tabs = new OsuTabControl<BeatmapDetailTab>
Child = tabs = new OsuTabControl<BeatmapDetailAreaTabItem>
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
@ -68,29 +80,22 @@ namespace osu.Game.Screens.Select
private void load(OsuColour colour, OsuConfigManager config)
{
modsCheckbox.AccentColour = tabs.AccentColour = colour.YellowLight;
selectedTab = config.GetBindable<BeatmapDetailTab>(OsuSetting.BeatmapDetailTab);
tabs.Current.BindTo(selectedTab);
tabs.Current.TriggerChange();
}
private void invokeOnFilter()
{
OnFilter?.Invoke(tabs.Current.Value, modsCheckbox.Current.Value);
modsCheckbox.FadeTo(tabs.Current.Value == BeatmapDetailTab.Details ? 0 : 1, 200, Easing.OutQuint);
tabsContainer.Padding = new MarginPadding { Right = tabs.Current.Value == BeatmapDetailTab.Details ? 0 : 100 };
if (tabs.Current.Value.FilterableByMods)
{
modsCheckbox.FadeTo(1, 200, Easing.OutQuint);
tabsContainer.Padding = new MarginPadding { Right = 100 };
}
else
{
modsCheckbox.FadeTo(0, 200, Easing.OutQuint);
tabsContainer.Padding = new MarginPadding();
}
}
}
public enum BeatmapDetailTab
{
Details,
Local,
Country,
Global,
Friends
}
}

View File

@ -0,0 +1,35 @@
// 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;
namespace osu.Game.Screens.Select
{
public abstract class BeatmapDetailAreaTabItem : IEquatable<BeatmapDetailAreaTabItem>
{
/// <summary>
/// The name of this tab, to be displayed in the tab control.
/// </summary>
public abstract string Name { get; }
/// <summary>
/// Whether the contents of this tab can be filtered by the user's currently-selected mods.
/// </summary>
public virtual bool FilterableByMods => false;
public override string ToString() => Name;
public bool Equals(BeatmapDetailAreaTabItem other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Name == other.Name;
}
public override int GetHashCode()
{
return Name != null ? Name.GetHashCode() : 0;
}
}
}

View File

@ -91,7 +91,7 @@ namespace osu.Game.Screens.Select
public FooterModDisplay()
{
AllowExpand = false;
ExpansionMode = ExpansionMode.AlwaysContracted;
IconsContainer.Margin = new MarginPadding();
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -21,11 +22,11 @@ namespace osu.Game.Screens.Select
public string ShortTitle => "song selection";
public override string Title => ShortTitle.Humanize();
[Resolved(typeof(Room))]
protected Bindable<PlaylistItem> CurrentItem { get; private set; }
public override bool AllowEditing => false;
[Resolved(typeof(Room), nameof(Room.Playlist))]
protected BindableList<PlaylistItem> Playlist { get; private set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
@ -34,12 +35,14 @@ namespace osu.Game.Screens.Select
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
}
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); // Todo: Temporary
protected override bool OnStart()
{
var item = new PlaylistItem
{
Beatmap = Beatmap.Value.BeatmapInfo,
Ruleset = Ruleset.Value,
Beatmap = { Value = Beatmap.Value.BeatmapInfo },
Ruleset = { Value = Ruleset.Value },
RulesetID = Ruleset.Value.ID ?? 0
};
@ -58,11 +61,13 @@ namespace osu.Game.Screens.Select
if (base.OnExiting(next))
return true;
if (CurrentItem.Value != null)
var firstItem = Playlist.FirstOrDefault();
if (firstItem != null)
{
Ruleset.Value = CurrentItem.Value.Ruleset;
Beatmap.Value = beatmaps.GetWorkingBeatmap(CurrentItem.Value.Beatmap);
Mods.Value = CurrentItem.Value.RequiredMods?.ToArray() ?? Array.Empty<Mod>();
Ruleset.Value = firstItem.Ruleset.Value;
Beatmap.Value = beatmaps.GetWorkingBeatmap(firstItem.Beatmap.Value);
Mods.Value = firstItem.RequiredMods?.ToArray() ?? Array.Empty<Mod>();
}
return false;

View File

@ -0,0 +1,143 @@
// 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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Screens.Select.Leaderboards;
namespace osu.Game.Screens.Select
{
public class PlayBeatmapDetailArea : BeatmapDetailArea
{
public readonly BeatmapLeaderboard Leaderboard;
public override WorkingBeatmap Beatmap
{
get => base.Beatmap;
set
{
base.Beatmap = value;
Leaderboard.Beatmap = value is DummyWorkingBeatmap ? null : value?.BeatmapInfo;
}
}
private Bindable<TabType> selectedTab;
public PlayBeatmapDetailArea()
{
Add(Leaderboard = new BeatmapLeaderboard { RelativeSizeAxes = Axes.Both });
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
selectedTab = config.GetBindable<TabType>(OsuSetting.BeatmapDetailTab);
selectedTab.BindValueChanged(tab => CurrentTab.Value = getTabItemFromTabType(tab.NewValue), true);
CurrentTab.BindValueChanged(tab => selectedTab.Value = getTabTypeFromTabItem(tab.NewValue));
}
public override void Refresh()
{
base.Refresh();
Leaderboard.RefreshScores();
}
protected override void OnTabChanged(BeatmapDetailAreaTabItem tab, bool selectedMods)
{
base.OnTabChanged(tab, selectedMods);
Leaderboard.FilterMods = selectedMods;
switch (tab)
{
case BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope> leaderboard:
Leaderboard.Scope = leaderboard.Scope;
Leaderboard.Show();
break;
default:
Leaderboard.Hide();
break;
}
}
protected override BeatmapDetailAreaTabItem[] CreateTabItems() => base.CreateTabItems().Concat(new BeatmapDetailAreaTabItem[]
{
new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local),
new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Country),
new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Global),
new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Friend),
}).ToArray();
private BeatmapDetailAreaTabItem getTabItemFromTabType(TabType type)
{
switch (type)
{
case TabType.Details:
return new BeatmapDetailAreaDetailTabItem();
case TabType.Local:
return new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local);
case TabType.Country:
return new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Country);
case TabType.Global:
return new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Global);
case TabType.Friends:
return new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Friend);
default:
throw new ArgumentOutOfRangeException(nameof(type));
}
}
private TabType getTabTypeFromTabItem(BeatmapDetailAreaTabItem item)
{
switch (item)
{
case BeatmapDetailAreaDetailTabItem _:
return TabType.Details;
case BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope> leaderboardTab:
switch (leaderboardTab.Scope)
{
case BeatmapLeaderboardScope.Local:
return TabType.Local;
case BeatmapLeaderboardScope.Country:
return TabType.Country;
case BeatmapLeaderboardScope.Global:
return TabType.Global;
case BeatmapLeaderboardScope.Friend:
return TabType.Friends;
default:
throw new ArgumentOutOfRangeException(nameof(item));
}
default:
throw new ArgumentOutOfRangeException(nameof(item));
}
}
public enum TabType
{
Details,
Local,
Country,
Global,
Friends
}
}
}

View File

@ -29,8 +29,12 @@ namespace osu.Game.Screens.Select
ValidForResume = false;
Edit();
}, Key.Number4);
((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += score => this.Push(new SoloResults(score));
}
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
public override void OnResuming(IScreen last)
{
base.OnResuming(last);

View File

@ -23,7 +23,6 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
using osu.Game.Screens.Select.Options;
using osu.Game.Skinning;
using osuTK;
@ -207,11 +206,11 @@ namespace osu.Game.Screens.Select
Left = left_area_padding,
Right = left_area_padding * 2,
},
Child = BeatmapDetails = new BeatmapDetailArea
Child = BeatmapDetails = CreateBeatmapDetailArea().With(d =>
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 10, Right = 5 },
},
d.RelativeSizeAxes = Axes.Both;
d.Padding = new MarginPadding { Top = 10, Right = 5 };
})
},
}
},
@ -262,8 +261,6 @@ namespace osu.Game.Screens.Select
});
}
BeatmapDetails.Leaderboard.ScoreSelected += score => this.Push(new SoloResults(score));
if (Footer != null)
{
Footer.AddButton(new FooterButtonMods { Current = Mods }, ModSelect);
@ -319,6 +316,11 @@ namespace osu.Game.Screens.Select
return dependencies;
}
/// <summary>
/// Creates the beatmap details to be displayed underneath the wedge.
/// </summary>
protected abstract BeatmapDetailArea CreateBeatmapDetailArea();
public void Edit(BeatmapInfo beatmap = null)
{
if (!AllowEditing)
@ -533,7 +535,7 @@ namespace osu.Game.Screens.Select
Carousel.AllowSelection = true;
BeatmapDetails.Leaderboard.RefreshScores();
BeatmapDetails.Refresh();
Beatmap.Value.Track.Looping = true;
music?.ResetTrackAdjustments();
@ -716,7 +718,7 @@ namespace osu.Game.Screens.Select
dialogOverlay?.Push(new BeatmapClearScoresDialog(beatmap, () =>
// schedule done here rather than inside the dialog as the dialog may fade out and never callback.
Schedule(() => BeatmapDetails.Leaderboard.RefreshScores())));
Schedule(() => BeatmapDetails.Refresh())));
}
public virtual bool OnPressed(GlobalAction action)

View File

@ -191,9 +191,9 @@ namespace osu.Game.Tests.Visual
track = audio?.Tracks.GetVirtual(length);
}
protected override void Dispose(bool isDisposing)
~ClockBackedTestWorkingBeatmap()
{
base.Dispose(isDisposing);
// Remove the track store from the audio manager
store?.Dispose();
}

View File

@ -26,11 +26,12 @@ namespace osu.Game.Users
{
public class UserPanel : OsuClickableContainer, IHasContextMenu
{
private readonly User user;
private const float height = 100;
private const float content_padding = 10;
private const float status_height = 30;
public readonly User User;
[Resolved(canBeNull: true)]
private OsuColour colours { get; set; }
@ -54,7 +55,7 @@ namespace osu.Game.Users
if (user == null)
throw new ArgumentNullException(nameof(user));
this.user = user;
User = user;
Height = height - status_height;
}
@ -86,7 +87,7 @@ namespace osu.Game.Users
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
User = user,
User = User,
}, 300, 5000)
{
RelativeSizeAxes = Axes.Both,
@ -106,7 +107,7 @@ namespace osu.Game.Users
new UpdateableAvatar
{
Size = new Vector2(height - status_height - content_padding * 2),
User = user,
User = User,
Masking = true,
CornerRadius = 5,
OpenOnClick = { Value = false },
@ -125,7 +126,7 @@ namespace osu.Game.Users
{
new OsuSpriteText
{
Text = user.Username,
Text = User.Username,
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 18, italics: true),
},
infoContainer = new FillFlowContainer
@ -138,7 +139,7 @@ namespace osu.Game.Users
Spacing = new Vector2(5f, 0f),
Children = new Drawable[]
{
new UpdateableFlag(user.Country)
new UpdateableFlag(User.Country)
{
Width = 30f,
RelativeSizeAxes = Axes.Y,
@ -191,12 +192,12 @@ namespace osu.Game.Users
}
});
if (user.IsSupporter)
if (User.IsSupporter)
{
infoContainer.Add(new SupporterIcon
{
Height = 20f,
SupportLevel = user.SupportLevel
SupportLevel = User.SupportLevel
});
}
@ -206,7 +207,7 @@ namespace osu.Game.Users
base.Action = ViewProfile = () =>
{
Action?.Invoke();
profile?.ShowUser(user);
profile?.ShowUser(User);
};
}

View File

@ -23,7 +23,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1230.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.207.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.213.0" />
<PackageReference Include="Sentry" Version="2.0.2" />
<PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.12.0" />

View File

@ -74,7 +74,7 @@
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1230.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.207.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.213.0" />
</ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
<ItemGroup Label="Transitive Dependencies">
@ -82,7 +82,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.207.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.213.0" />
<PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />