1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-22 17:22:58 +08:00

Merge branch 'master' into editor-slider-control-point-quick-delete

This commit is contained in:
Dan Balasescu 2020-11-05 00:38:42 +09:00 committed by GitHub
commit ea2fd831ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 729 additions and 157 deletions

2
.gitignore vendored
View File

@ -334,3 +334,5 @@ inspectcode
# BenchmarkDotNet # BenchmarkDotNet
/BenchmarkDotNet.Artifacts /BenchmarkDotNet.Artifacts
*.GeneratedMSBuildEditorConfig.editorconfig

View File

@ -4,5 +4,6 @@ M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(
M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead. M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead.
T:System.IComparable;Don't use non-generic IComparable. Use generic version instead. T:System.IComparable;Don't use non-generic IComparable. Use generic version instead.
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText. M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods. T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.
T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods. T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods.

View File

@ -62,6 +62,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public abstract class DrawableCatchHitObject : DrawableHitObject<CatchHitObject> public abstract class DrawableCatchHitObject : DrawableHitObject<CatchHitObject>
{ {
protected override double InitialLifetimeOffset => HitObject.TimePreempt;
public virtual bool StaysOnPlate => HitObject.CanBePlated; public virtual bool StaysOnPlate => HitObject.CanBePlated;
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;

View File

@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Replays
if (Position > lastCatchFrame.Position) if (Position > lastCatchFrame.Position)
lastCatchFrame.Actions.Add(CatchAction.MoveRight); lastCatchFrame.Actions.Add(CatchAction.MoveRight);
else if (Position < lastCatchFrame.Position) else if (Position < lastCatchFrame.Position)
Actions.Add(CatchAction.MoveLeft); lastCatchFrame.Actions.Add(CatchAction.MoveLeft);
} }
} }

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Audio
private readonly ControlPointInfo controlPoints; private readonly ControlPointInfo controlPoints;
private readonly Dictionary<double, DrumSample> mappings = new Dictionary<double, DrumSample>(); private readonly Dictionary<double, DrumSample> mappings = new Dictionary<double, DrumSample>();
private IBindableList<SampleControlPoint> samplePoints; private readonly IBindableList<SampleControlPoint> samplePoints = new BindableList<SampleControlPoint>();
public DrumSampleContainer(ControlPointInfo controlPoints) public DrumSampleContainer(ControlPointInfo controlPoints)
{ {
@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Audio
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
samplePoints = controlPoints.SamplePoints.GetBoundCopy(); samplePoints.BindTo(controlPoints.SamplePoints);
samplePoints.BindCollectionChanged((_, __) => recreateMappings(), true); samplePoints.BindCollectionChanged((_, __) => recreateMappings(), true);
} }

View File

@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Editing
AddToggleStep("toggle y", state => selectionBox.CanScaleY = state); AddToggleStep("toggle y", state => selectionBox.CanScaleY = state);
} }
private void handleScale(Vector2 amount, Anchor reference) private bool handleScale(Vector2 amount, Anchor reference)
{ {
if ((reference & Anchor.y1) == 0) if ((reference & Anchor.y1) == 0)
{ {
@ -58,12 +58,15 @@ namespace osu.Game.Tests.Visual.Editing
selectionArea.X += amount.X; selectionArea.X += amount.X;
selectionArea.Width += directionX * amount.X; selectionArea.Width += directionX * amount.X;
} }
return true;
} }
private void handleRotation(float angle) private bool handleRotation(float angle)
{ {
// kinda silly and wrong, but just showing that the drag handles work. // kinda silly and wrong, but just showing that the drag handles work.
selectionArea.Rotation += angle; selectionArea.Rotation += angle;
return true;
} }
} }
} }

View File

@ -7,13 +7,14 @@ using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
{ {
@ -88,6 +89,7 @@ namespace osu.Game.Tests.Visual.Editing
// Scroll in at 0.25 // Scroll in at 0.25
AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y))); AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y)));
AddStep("Press alt down", () => InputManager.PressKey(Key.AltLeft));
AddStep("Scroll by 3", () => InputManager.ScrollBy(new Vector2(0, 3))); AddStep("Scroll by 3", () => InputManager.ScrollBy(new Vector2(0, 3)));
AddAssert("Box not at 0", () => !Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); AddAssert("Box not at 0", () => !Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft));
AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X)); AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X));
@ -96,6 +98,25 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Scroll by -3", () => InputManager.ScrollBy(new Vector2(0, -3))); AddStep("Scroll by -3", () => InputManager.ScrollBy(new Vector2(0, -3)));
AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft));
AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X)); AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X));
AddStep("Release alt", () => InputManager.ReleaseKey(Key.AltLeft));
}
[Test]
public void TestMouseZoomInThenScroll()
{
reset();
// Scroll in at 0.25
AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y)));
AddStep("Press alt down", () => InputManager.PressKey(Key.AltLeft));
AddStep("Zoom by 3", () => InputManager.ScrollBy(new Vector2(0, 3)));
AddStep("Release alt", () => InputManager.ReleaseKey(Key.AltLeft));
AddStep("Scroll far left", () => InputManager.ScrollBy(new Vector2(0, 30)));
AddUntilStep("Scroll is at start", () => Precision.AlmostEquals(scrollQuad.TopLeft.X, boxQuad.TopLeft.X, 1));
AddStep("Scroll far right", () => InputManager.ScrollBy(new Vector2(0, -300)));
AddUntilStep("Scroll is at end", () => Precision.AlmostEquals(scrollQuad.TopRight.X, boxQuad.TopRight.X, 1));
} }
[Test] [Test]
@ -103,6 +124,8 @@ namespace osu.Game.Tests.Visual.Editing
{ {
reset(); reset();
AddStep("Press alt down", () => InputManager.PressKey(Key.AltLeft));
// Scroll in at 0.25 // Scroll in at 0.25
AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y))); AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y)));
AddStep("Scroll by 1", () => InputManager.ScrollBy(new Vector2(0, 1))); AddStep("Scroll by 1", () => InputManager.ScrollBy(new Vector2(0, 1)));
@ -124,6 +147,8 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y))); AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y)));
AddStep("Scroll by -1", () => InputManager.ScrollBy(new Vector2(0, -1))); AddStep("Scroll by -1", () => InputManager.ScrollBy(new Vector2(0, -1)));
AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft));
AddStep("Release alt", () => InputManager.ReleaseKey(Key.AltLeft));
} }
private void reset() private void reset()

View File

@ -3,8 +3,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -227,12 +229,19 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded);
} }
internal class TestSpectatorStreamingClient : SpectatorStreamingClient public class TestSpectatorStreamingClient : SpectatorStreamingClient
{ {
public readonly User StreamingUser = new User { Id = 1234, Username = "Test user" }; public readonly User StreamingUser = new User { Id = 1234, Username = "Test user" };
public new BindableList<int> PlayingUsers => (BindableList<int>)base.PlayingUsers;
private int beatmapId; private int beatmapId;
protected override Task Connect()
{
return Task.CompletedTask;
}
public void StartPlay(int beatmapId) public void StartPlay(int beatmapId)
{ {
this.beatmapId = beatmapId; this.beatmapId = beatmapId;

View File

@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private Replay replay; private Replay replay;
private IBindableList<int> users; private readonly IBindableList<int> users = new BindableList<int>();
private TestReplayRecorder recorder; private TestReplayRecorder recorder;
@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
replay = new Replay(); replay = new Replay();
users = streamingClient.PlayingUsers.GetBoundCopy(); users.BindTo(streamingClient.PlayingUsers);
users.BindCollectionChanged((obj, args) => users.BindCollectionChanged((obj, args) =>
{ {
switch (args.Action) switch (args.Action)

View File

@ -0,0 +1,66 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Spectator;
using osu.Game.Overlays.Dashboard;
using osu.Game.Tests.Visual.Gameplay;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneCurrentlyPlayingDisplay : OsuTestScene
{
[Cached(typeof(SpectatorStreamingClient))]
private TestSceneSpectator.TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSceneSpectator.TestSpectatorStreamingClient();
private CurrentlyPlayingDisplay currentlyPlaying;
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("register request handling", () => dummyAPI.HandleRequest = req =>
{
switch (req)
{
case GetUserRequest cRequest:
cRequest.TriggerSuccess(new User { Username = "peppy", Id = 2 });
break;
}
});
AddStep("add streaming client", () =>
{
Remove(testSpectatorStreamingClient);
Children = new Drawable[]
{
testSpectatorStreamingClient,
currentlyPlaying = new CurrentlyPlayingDisplay
{
RelativeSizeAxes = Axes.Both,
}
};
});
AddStep("Reset players", () => testSpectatorStreamingClient.PlayingUsers.Clear());
}
[Test]
public void TestBasicDisplay()
{
AddStep("Add playing user", () => testSpectatorStreamingClient.PlayingUsers.Add(2));
AddUntilStep("Panel loaded", () => currentlyPlaying.ChildrenOfType<UserGridPanel>()?.FirstOrDefault()?.User.Id == 2);
AddStep("Remove playing user", () => testSpectatorStreamingClient.PlayingUsers.Remove(2));
AddUntilStep("Panel no longer present", () => !currentlyPlaying.ChildrenOfType<UserGridPanel>().Any());
}
}
}

View File

@ -42,6 +42,8 @@ namespace osu.Game.Configuration
Set(OsuSetting.Username, string.Empty); Set(OsuSetting.Username, string.Empty);
Set(OsuSetting.Token, string.Empty); Set(OsuSetting.Token, string.Empty);
Set(OsuSetting.AutomaticallyDownloadWhenSpectating, false);
Set(OsuSetting.SavePassword, false).ValueChanged += enabled => Set(OsuSetting.SavePassword, false).ValueChanged += enabled =>
{ {
if (enabled.NewValue) Set(OsuSetting.SaveUsername, true); if (enabled.NewValue) Set(OsuSetting.SaveUsername, true);
@ -132,6 +134,8 @@ namespace osu.Game.Configuration
Set(OsuSetting.MenuBackgroundSource, BackgroundSource.Skin); Set(OsuSetting.MenuBackgroundSource, BackgroundSource.Skin);
Set(OsuSetting.SeasonalBackgroundMode, SeasonalBackgroundMode.Sometimes); Set(OsuSetting.SeasonalBackgroundMode, SeasonalBackgroundMode.Sometimes);
Set(OsuSetting.EditorWaveformOpacity, 1f);
} }
public OsuConfigManager(Storage storage) public OsuConfigManager(Storage storage)
@ -241,6 +245,8 @@ namespace osu.Game.Configuration
HitLighting, HitLighting,
MenuBackgroundSource, MenuBackgroundSource,
GameplayDisableWinKey, GameplayDisableWinKey,
SeasonalBackgroundMode SeasonalBackgroundMode,
EditorWaveformOpacity,
AutomaticallyDownloadWhenSpectating,
} }
} }

View File

@ -93,14 +93,14 @@ namespace osu.Game.Online.Spectator
break; break;
case APIState.Online: case APIState.Online:
Task.Run(connect); Task.Run(Connect);
break; break;
} }
} }
private const string endpoint = "https://spectator.ppy.sh/spectator"; private const string endpoint = "https://spectator.ppy.sh/spectator";
private async Task connect() protected virtual async Task Connect()
{ {
if (connection != null) if (connection != null)
return; return;

View File

@ -0,0 +1,132 @@
// 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.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Spectator;
using osu.Game.Screens.Multi.Match.Components;
using osu.Game.Screens.Play;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Overlays.Dashboard
{
internal class CurrentlyPlayingDisplay : CompositeDrawable
{
private readonly IBindableList<int> playingUsers = new BindableList<int>();
private FillFlowContainer<PlayingUserPanel> userFlow;
[Resolved]
private SpectatorStreamingClient spectatorStreaming { get; set; }
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = userFlow = new FillFlowContainer<PlayingUserPanel>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10),
};
}
[Resolved]
private IAPIProvider api { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
playingUsers.BindTo(spectatorStreaming.PlayingUsers);
playingUsers.BindCollectionChanged((sender, e) => Schedule(() =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var u in e.NewItems.OfType<int>())
{
var request = new GetUserRequest(u);
request.Success += user => Schedule(() =>
{
if (playingUsers.Contains((int)user.Id))
userFlow.Add(createUserPanel(user));
});
api.Queue(request);
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (var u in e.OldItems.OfType<int>())
userFlow.FirstOrDefault(card => card.User.Id == u)?.Expire();
break;
case NotifyCollectionChangedAction.Reset:
userFlow.Clear();
break;
}
}), true);
}
private PlayingUserPanel createUserPanel(User user) =>
new PlayingUserPanel(user).With(panel =>
{
panel.Anchor = Anchor.TopCentre;
panel.Origin = Anchor.TopCentre;
});
private class PlayingUserPanel : CompositeDrawable
{
public readonly User User;
[Resolved(canBeNull: true)]
private OsuGame game { get; set; }
public PlayingUserPanel(User user)
{
User = user;
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2),
Width = 290,
Children = new Drawable[]
{
new UserGridPanel(user)
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
new PurpleTriangleButton
{
RelativeSizeAxes = Axes.X,
Text = "Watch",
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Action = () => game?.PerformFromScreen(s => s.Push(new Spectator(user)))
}
}
},
};
}
}
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.ComponentModel;
namespace osu.Game.Overlays.Dashboard namespace osu.Game.Overlays.Dashboard
{ {
public class DashboardOverlayHeader : TabControlOverlayHeader<DashboardOverlayTabs> public class DashboardOverlayHeader : TabControlOverlayHeader<DashboardOverlayTabs>
@ -20,6 +22,9 @@ namespace osu.Game.Overlays.Dashboard
public enum DashboardOverlayTabs public enum DashboardOverlayTabs
{ {
Friends Friends,
[Description("Currently Playing")]
CurrentlyPlaying
} }
} }

View File

@ -130,6 +130,11 @@ namespace osu.Game.Overlays
loadDisplay(new FriendDisplay()); loadDisplay(new FriendDisplay());
break; break;
case DashboardOverlayTabs.CurrentlyPlaying:
//todo: enable once caching logic is better
//loadDisplay(new CurrentlyPlayingDisplay());
break;
default: default:
throw new NotImplementedException($"Display for {tab.NewValue} tab is not implemented"); throw new NotImplementedException($"Display for {tab.NewValue} tab is not implemented");
} }

View File

@ -27,6 +27,12 @@ namespace osu.Game.Overlays.Settings.Sections.Online
Keywords = new[] { "no-video" }, Keywords = new[] { "no-video" },
Current = config.GetBindable<bool>(OsuSetting.PreferNoVideo) Current = config.GetBindable<bool>(OsuSetting.PreferNoVideo)
}, },
new SettingsCheckbox
{
LabelText = "Automatically download beatmaps when spectating",
Keywords = new[] { "spectator" },
Current = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadWhenSpectating),
},
}; };
} }
} }

View File

@ -1,9 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -104,7 +106,7 @@ namespace osu.Game.Overlays
public OverlayHeaderTabItem(T value) public OverlayHeaderTabItem(T value)
: base(value) : base(value)
{ {
Text.Text = value.ToString().ToLower(); Text.Text = ((Value as Enum)?.GetDescription() ?? Value.ToString()).ToLower();
Text.Font = OsuFont.GetFont(size: 14); Text.Font = OsuFont.GetFont(size: 14);
Text.Margin = new MarginPadding { Vertical = 16.5f }; // 15px padding + 1.5px line-height difference compensation Text.Margin = new MarginPadding { Vertical = 16.5f }; // 15px padding + 1.5px line-height difference compensation
Bar.Margin = new MarginPadding { Bottom = bar_height }; Bar.Margin = new MarginPadding { Bottom = bar_height };

View File

@ -163,30 +163,27 @@ namespace osu.Game.Screens.Edit.Components.Menus
protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu(); protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu();
protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableSubMenuItem(item); protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item)
private class DrawableSubMenuItem : DrawableOsuMenuItem
{ {
public DrawableSubMenuItem(MenuItem item) switch (item)
{
case EditorMenuItemSpacer spacer:
return new DrawableSpacer(spacer);
}
return base.CreateDrawableMenuItem(item);
}
private class DrawableSpacer : DrawableOsuMenuItem
{
public DrawableSpacer(MenuItem item)
: base(item) : base(item)
{ {
} }
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e) => true;
{
if (Item is EditorMenuItemSpacer)
return true;
return base.OnHover(e); protected override bool OnClick(ClickEvent e) => true;
}
protected override bool OnClick(ClickEvent e)
{
if (Item is EditorMenuItemSpacer)
return true;
return base.OnClick(e);
}
} }
} }
} }

View File

@ -14,13 +14,13 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
/// </summary> /// </summary>
public class ControlPointPart : TimelinePart<GroupVisualisation> public class ControlPointPart : TimelinePart<GroupVisualisation>
{ {
private IBindableList<ControlPointGroup> controlPointGroups; private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
protected override void LoadBeatmap(WorkingBeatmap beatmap) protected override void LoadBeatmap(WorkingBeatmap beatmap)
{ {
base.LoadBeatmap(beatmap); base.LoadBeatmap(beatmap);
controlPointGroups = beatmap.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); controlPointGroups.BindTo(beatmap.Beatmap.ControlPointInfo.Groups);
controlPointGroups.BindCollectionChanged((sender, args) => controlPointGroups.BindCollectionChanged((sender, args) =>
{ {
switch (args.Action) switch (args.Action)

View File

@ -15,7 +15,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{ {
public readonly ControlPointGroup Group; public readonly ControlPointGroup Group;
private BindableList<ControlPoint> controlPoints; private readonly IBindableList<ControlPoint> controlPoints = new BindableList<ControlPoint>();
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
@ -30,7 +30,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{ {
base.LoadComplete(); base.LoadComplete();
controlPoints = (BindableList<ControlPoint>)Group.ControlPoints.GetBoundCopy(); controlPoints.BindTo(Group.ControlPoints);
controlPoints.BindCollectionChanged((_, __) => controlPoints.BindCollectionChanged((_, __) =>
{ {
if (controlPoints.Count == 0) if (controlPoints.Count == 0)

View File

@ -7,17 +7,19 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components namespace osu.Game.Screens.Edit.Compose.Components
{ {
public class SelectionBox : CompositeDrawable public class SelectionBox : CompositeDrawable
{ {
public Action<float> OnRotation; public Func<float, bool> OnRotation;
public Action<Vector2, Anchor> OnScale; public Func<Vector2, Anchor, bool> OnScale;
public Action<Direction> OnFlip; public Func<Direction, bool> OnFlip;
public Action OnReverse; public Func<bool> OnReverse;
public Action OperationStarted; public Action OperationStarted;
public Action OperationEnded; public Action OperationEnded;
@ -105,6 +107,26 @@ namespace osu.Game.Screens.Edit.Compose.Components
recreate(); recreate();
} }
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Repeat || !e.ControlPressed)
return false;
switch (e.Key)
{
case Key.G:
return CanReverse && OnReverse?.Invoke() == true;
case Key.H:
return CanScaleX && OnFlip?.Invoke(Direction.Horizontal) == true;
case Key.J:
return CanScaleY && OnFlip?.Invoke(Direction.Vertical) == true;
}
return base.OnKeyDown(e);
}
private void recreate() private void recreate()
{ {
if (LoadState < LoadState.Loading) if (LoadState < LoadState.Loading)
@ -143,7 +165,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (CanScaleX && CanScaleY) addFullScaleComponents(); if (CanScaleX && CanScaleY) addFullScaleComponents();
if (CanScaleY) addYScaleComponents(); if (CanScaleY) addYScaleComponents();
if (CanRotate) addRotationComponents(); if (CanRotate) addRotationComponents();
if (CanReverse) addButton(FontAwesome.Solid.Backward, "Reverse pattern", () => OnReverse?.Invoke()); if (CanReverse) addButton(FontAwesome.Solid.Backward, "Reverse pattern (Ctrl-G)", () => OnReverse?.Invoke());
} }
private void addRotationComponents() private void addRotationComponents()
@ -178,7 +200,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void addYScaleComponents() private void addYScaleComponents()
{ {
addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically", () => OnFlip?.Invoke(Direction.Vertical)); addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically (Ctrl-J)", () => OnFlip?.Invoke(Direction.Vertical));
addDragHandle(Anchor.TopCentre); addDragHandle(Anchor.TopCentre);
addDragHandle(Anchor.BottomCentre); addDragHandle(Anchor.BottomCentre);
@ -194,7 +216,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void addXScaleComponents() private void addXScaleComponents()
{ {
addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally", () => OnFlip?.Invoke(Direction.Horizontal)); addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally (Ctrl-H)", () => OnFlip?.Invoke(Direction.Horizontal));
addDragHandle(Anchor.CentreLeft); addDragHandle(Anchor.CentreLeft);
addDragHandle(Anchor.CentreRight); addDragHandle(Anchor.CentreRight);

View File

@ -99,10 +99,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
OperationStarted = OnOperationBegan, OperationStarted = OnOperationBegan,
OperationEnded = OnOperationEnded, OperationEnded = OnOperationEnded,
OnRotation = angle => HandleRotation(angle), OnRotation = HandleRotation,
OnScale = (amount, anchor) => HandleScale(amount, anchor), OnScale = HandleScale,
OnFlip = direction => HandleFlip(direction), OnFlip = HandleFlip,
OnReverse = () => HandleReverse(), OnReverse = HandleReverse,
}; };
/// <summary> /// <summary>

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osuTK; using osuTK;
@ -67,8 +68,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private TimelineControlPointDisplay controlPoints; private TimelineControlPointDisplay controlPoints;
private Bindable<float> waveformOpacity;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours) private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours, OsuConfigManager config)
{ {
AddRange(new Drawable[] AddRange(new Drawable[]
{ {
@ -95,7 +98,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
// We don't want the centre marker to scroll // We don't want the centre marker to scroll
AddInternal(new CentreMarker { Depth = float.MaxValue }); AddInternal(new CentreMarker { Depth = float.MaxValue });
WaveformVisible.ValueChanged += visible => waveform.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint); waveformOpacity = config.GetBindable<float>(OsuSetting.EditorWaveformOpacity);
waveformOpacity.BindValueChanged(_ => updateWaveformOpacity(), true);
WaveformVisible.ValueChanged += _ => updateWaveformOpacity();
ControlPointsVisible.ValueChanged += visible => controlPoints.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint); ControlPointsVisible.ValueChanged += visible => controlPoints.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
TicksVisible.ValueChanged += visible => ticks.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint); TicksVisible.ValueChanged += visible => ticks.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
@ -115,6 +121,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}, true); }, true);
} }
private void updateWaveformOpacity() =>
waveform.FadeTo(WaveformVisible.Value ? waveformOpacity.Value : 0, 200, Easing.OutQuint);
private float getZoomLevelForVisibleMilliseconds(double milliseconds) => Math.Max(1, (float)(track.Length / milliseconds)); private float getZoomLevelForVisibleMilliseconds(double milliseconds) => Math.Max(1, (float)(track.Length / milliseconds));
protected override void Update() protected override void Update()
@ -165,6 +174,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (!track.IsLoaded || track.Length == 0) if (!track.IsLoaded || track.Length == 0)
return; return;
// covers the case where the user starts playback after a drag is in progress.
// we want to ensure the clock is always stopped during drags to avoid weird audio playback.
if (handlingDragInput)
editorClock.Stop();
ScrollTo((float)(editorClock.CurrentTime / track.Length) * Content.DrawWidth, false); ScrollTo((float)(editorClock.CurrentTime / track.Length) * Content.DrawWidth, false);
} }

View File

@ -16,7 +16,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
/// </summary> /// </summary>
public class TimelineControlPointDisplay : TimelinePart<TimelineControlPointGroup> public class TimelineControlPointDisplay : TimelinePart<TimelineControlPointGroup>
{ {
private IBindableList<ControlPointGroup> controlPointGroups; private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
public TimelineControlPointDisplay() public TimelineControlPointDisplay()
{ {
@ -27,7 +27,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
base.LoadBeatmap(beatmap); base.LoadBeatmap(beatmap);
controlPointGroups = beatmap.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); controlPointGroups.BindTo(beatmap.Beatmap.ControlPointInfo.Groups);
controlPointGroups.BindCollectionChanged((sender, args) => controlPointGroups.BindCollectionChanged((sender, args) =>
{ {
switch (args.Action) switch (args.Action)

View File

@ -14,7 +14,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
public readonly ControlPointGroup Group; public readonly ControlPointGroup Group;
private BindableList<ControlPoint> controlPoints; private readonly IBindableList<ControlPoint> controlPoints = new BindableList<ControlPoint>();
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
@ -34,7 +34,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
base.LoadComplete(); base.LoadComplete();
controlPoints = (BindableList<ControlPoint>)Group.ControlPoints.GetBoundCopy(); controlPoints.BindTo(Group.ControlPoints);
controlPoints.BindCollectionChanged((_, __) => controlPoints.BindCollectionChanged((_, __) =>
{ {
ClearInternal(); ClearInternal();

View File

@ -113,19 +113,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override bool OnScroll(ScrollEvent e) protected override bool OnScroll(ScrollEvent e)
{ {
if (e.IsPrecise) if (e.AltPressed)
{ {
// can't handle scroll correctly while playing. // zoom when holding alt.
// the editor will handle this case for us. setZoomTarget(zoomTarget + e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X);
if (editorClock?.IsRunning == true) return 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); // can't handle scroll correctly while playing.
return true; // the editor will handle this case for us.
if (editorClock?.IsRunning == true)
return false;
return base.OnScroll(e);
} }
private void updateZoomedContentWidth() => zoomedContent.Width = DrawWidth * currentZoom; private void updateZoomedContentWidth() => zoomedContent.Width = DrawWidth * currentZoom;

View File

@ -20,6 +20,7 @@ using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -103,7 +104,7 @@ namespace osu.Game.Screens.Edit
private MusicController music { get; set; } private MusicController music { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours, GameHost host) private void load(OsuColour colours, GameHost host, OsuConfigManager config)
{ {
beatDivisor.Value = Beatmap.Value.BeatmapInfo.BeatDivisor; beatDivisor.Value = Beatmap.Value.BeatmapInfo.BeatDivisor;
beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue); beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue);
@ -208,6 +209,13 @@ namespace osu.Game.Screens.Edit
copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy), copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy),
pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste), pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste),
} }
},
new MenuItem("View")
{
Items = new[]
{
new WaveformOpacityMenu(config)
}
} }
} }
} }

View File

@ -98,7 +98,7 @@ namespace osu.Game.Screens.Edit.Timing
private class ControlGroupAttributes : CompositeDrawable private class ControlGroupAttributes : CompositeDrawable
{ {
private readonly IBindableList<ControlPoint> controlPoints; private readonly IBindableList<ControlPoint> controlPoints = new BindableList<ControlPoint>();
private readonly FillFlowContainer fill; private readonly FillFlowContainer fill;
@ -112,7 +112,7 @@ namespace osu.Game.Screens.Edit.Timing
Spacing = new Vector2(2) Spacing = new Vector2(2)
}; };
controlPoints = group.ControlPoints.GetBoundCopy(); controlPoints.BindTo(group.ControlPoints);
} }
[Resolved] [Resolved]

View File

@ -56,7 +56,7 @@ namespace osu.Game.Screens.Edit.Timing
private OsuButton deleteButton; private OsuButton deleteButton;
private ControlPointTable table; private ControlPointTable table;
private IBindableList<ControlPointGroup> controlGroups; private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
[Resolved] [Resolved]
private EditorClock clock { get; set; } private EditorClock clock { get; set; }
@ -124,11 +124,10 @@ namespace osu.Game.Screens.Edit.Timing
selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true); selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true);
controlGroups = Beatmap.Value.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); controlPointGroups.BindTo(Beatmap.Value.Beatmap.ControlPointInfo.Groups);
controlPointGroups.BindCollectionChanged((sender, args) =>
controlGroups.BindCollectionChanged((sender, args) =>
{ {
table.ControlGroups = controlGroups; table.ControlGroups = controlPointGroups;
changeHandler.SaveState(); changeHandler.SaveState();
}, true); }, true);
} }

View File

@ -0,0 +1,46 @@
// 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.Bindables;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Edit
{
internal class WaveformOpacityMenu : MenuItem
{
private readonly Bindable<float> waveformOpacity;
private readonly Dictionary<float, ToggleMenuItem> menuItemLookup = new Dictionary<float, ToggleMenuItem>();
public WaveformOpacityMenu(OsuConfigManager config)
: base("Waveform opacity")
{
Items = new[]
{
createMenuItem(0.25f),
createMenuItem(0.5f),
createMenuItem(0.75f),
createMenuItem(1f),
};
waveformOpacity = config.GetBindable<float>(OsuSetting.EditorWaveformOpacity);
waveformOpacity.BindValueChanged(opacity =>
{
foreach (var kvp in menuItemLookup)
kvp.Value.State.Value = kvp.Key == opacity.NewValue;
}, true);
}
private ToggleMenuItem createMenuItem(float opacity)
{
var item = new ToggleMenuItem($"{opacity * 100}%", MenuItemType.Standard, _ => updateOpacity(opacity));
menuItemLookup[opacity] = item;
return item;
}
private void updateOpacity(float opacity) => waveformOpacity.Value = opacity;
}
}

View File

@ -9,20 +9,26 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Overlays.Settings;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Replays.Types;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Multi.Match.Components;
using osu.Game.Users; using osu.Game.Users;
using osuTK; using osuTK;
@ -63,6 +69,12 @@ namespace osu.Game.Screens.Play
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated; private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
private TriangleButton watchButton;
private SettingsCheckbox automaticDownload;
private BeatmapSetInfo onlineBeatmap;
/// <summary> /// <summary>
/// Becomes true if a new state is waiting to be loaded (while this screen was not active). /// Becomes true if a new state is waiting to be loaded (while this screen was not active).
/// </summary> /// </summary>
@ -74,47 +86,91 @@ namespace osu.Game.Screens.Play
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(OsuColour colours, OsuConfigManager config)
{ {
InternalChildren = new Drawable[] InternalChild = new Container
{ {
new FillFlowContainer Masking = true,
CornerRadius = 20,
AutoSizeAxes = Axes.Both,
AutoSizeDuration = 500,
AutoSizeEasing = Easing.OutQuint,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{ {
AutoSizeAxes = Axes.Both, new Box
Direction = FillDirection.Vertical,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spacing = new Vector2(15),
Children = new Drawable[]
{ {
new OsuSpriteText Colour = colours.GreySeafoamDark,
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
Margin = new MarginPadding(20),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spacing = new Vector2(15),
Children = new Drawable[]
{ {
Text = "Currently spectating", new OsuSpriteText
Font = OsuFont.Default.With(size: 30), {
Anchor = Anchor.Centre, Text = "Spectator Mode",
Origin = Anchor.Centre, Font = OsuFont.Default.With(size: 30),
}, Anchor = Anchor.Centre,
new UserGridPanel(targetUser) Origin = Anchor.Centre,
{ },
Width = 290, new FillFlowContainer
Anchor = Anchor.Centre, {
Origin = Anchor.Centre, AutoSizeAxes = Axes.Both,
}, Direction = FillDirection.Horizontal,
new OsuSpriteText Anchor = Anchor.Centre,
{ Origin = Anchor.Centre,
Text = "playing", Spacing = new Vector2(15),
Font = OsuFont.Default.With(size: 30), Children = new Drawable[]
Anchor = Anchor.Centre, {
Origin = Anchor.Centre, new UserGridPanel(targetUser)
}, {
beatmapPanelContainer = new Container Anchor = Anchor.CentreLeft,
{ Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both, Height = 145,
Anchor = Anchor.Centre, Width = 290,
Origin = Anchor.Centre, },
}, new SpriteIcon
{
Size = new Vector2(40),
Icon = FontAwesome.Solid.ArrowRight,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
beatmapPanelContainer = new Container
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
}
},
automaticDownload = new SettingsCheckbox
{
LabelText = "Automatically download beatmaps",
Current = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadWhenSpectating),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
watchButton = new PurpleTriangleButton
{
Text = "Start Watching",
Width = 250,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Action = attemptStart,
Enabled = { Value = false }
}
}
} }
}, }
}; };
} }
@ -130,12 +186,14 @@ namespace osu.Game.Screens.Play
managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); managerUpdated = beatmaps.ItemUpdated.GetBoundCopy();
managerUpdated.BindValueChanged(beatmapUpdated); managerUpdated.BindValueChanged(beatmapUpdated);
automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload());
} }
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> beatmap) private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> beatmap)
{ {
if (beatmap.NewValue.TryGetTarget(out var beatmapSet) && beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == state.BeatmapID)) if (beatmap.NewValue.TryGetTarget(out var beatmapSet) && beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == state.BeatmapID))
attemptStart(); Schedule(attemptStart);
} }
private void userSentFrames(int userId, FrameDataBundle data) private void userSentFrames(int userId, FrameDataBundle data)
@ -189,14 +247,26 @@ namespace osu.Game.Screens.Play
if (userId != targetUser.Id) if (userId != targetUser.Id)
return; return;
if (replay == null) return; if (replay != null)
{
replay.HasReceivedAllFrames = true;
replay = null;
}
replay.HasReceivedAllFrames = true; Schedule(clearDisplay);
replay = null; }
private void clearDisplay()
{
watchButton.Enabled.Value = false;
beatmapPanelContainer.Clear();
} }
private void attemptStart() private void attemptStart()
{ {
clearDisplay();
showBeatmapPanel(state);
var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == state.RulesetID)?.CreateInstance(); var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == state.RulesetID)?.CreateInstance();
// ruleset not available // ruleset not available
@ -210,7 +280,6 @@ namespace osu.Game.Screens.Play
if (resolvedBeatmap == null) if (resolvedBeatmap == null)
{ {
showBeatmapPanel(state.BeatmapID.Value);
return; return;
} }
@ -228,6 +297,7 @@ namespace osu.Game.Screens.Play
rulesetInstance = resolvedRuleset; rulesetInstance = resolvedRuleset;
beatmap.Value = beatmaps.GetWorkingBeatmap(resolvedBeatmap); beatmap.Value = beatmaps.GetWorkingBeatmap(resolvedBeatmap);
watchButton.Enabled.Value = true;
this.Push(new SpectatorPlayerLoader(new Score this.Push(new SpectatorPlayerLoader(new Score
{ {
@ -236,17 +306,43 @@ namespace osu.Game.Screens.Play
})); }));
} }
private void showBeatmapPanel(int beatmapId) private void showBeatmapPanel(SpectatorState state)
{ {
var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId); if (state?.BeatmapID == null)
{
beatmapPanelContainer.Clear();
onlineBeatmap = null;
return;
}
var req = new GetBeatmapSetRequest(state.BeatmapID.Value, BeatmapSetLookupType.BeatmapId);
req.Success += res => Schedule(() => req.Success += res => Schedule(() =>
{ {
beatmapPanelContainer.Child = new GridBeatmapPanel(res.ToBeatmapSet(rulesets)); if (state != this.state)
return;
onlineBeatmap = res.ToBeatmapSet(rulesets);
beatmapPanelContainer.Child = new GridBeatmapPanel(onlineBeatmap);
checkForAutomaticDownload();
}); });
api.Queue(req); api.Queue(req);
} }
private void checkForAutomaticDownload()
{
if (onlineBeatmap == null)
return;
if (!automaticDownload.Current.Value)
return;
if (beatmaps.IsAvailableLocally(onlineBeatmap))
return;
beatmaps.Download(onlineBeatmap);
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);

View File

@ -3,33 +3,60 @@
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
public class SpectatorPlayer : ReplayPlayer public class SpectatorPlayer : Player
{ {
[Resolved] private readonly Score score;
private SpectatorStreamingClient spectatorStreaming { get; set; }
protected override bool CheckModsAllowFailure() => false; // todo: better support starting mid-way through beatmap
public SpectatorPlayer(Score score) public SpectatorPlayer(Score score)
: base(score)
{ {
this.score = score;
} }
protected override ResultsScreen CreateResults(ScoreInfo score)
{
return new SpectatorResultsScreen(score);
}
[Resolved]
private SpectatorStreamingClient spectatorStreaming { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
spectatorStreaming.OnUserBeganPlaying += userBeganPlaying; spectatorStreaming.OnUserBeganPlaying += userBeganPlaying;
AddInternal(new OsuSpriteText
{
Text = $"Watching {score.ScoreInfo.User.Username} playing live!",
Font = OsuFont.Default.With(size: 30),
Y = 100,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
});
}
protected override void PrepareReplay()
{
DrawableRuleset?.SetReplayScore(score);
} }
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
{ {
// if we already have frames, start gameplay at the point in time they exist, should they be too far into the beatmap. // if we already have frames, start gameplay at the point in time they exist, should they be too far into the beatmap.
double? firstFrameTime = Score.Replay.Frames.FirstOrDefault()?.Time; double? firstFrameTime = score.Replay.Frames.FirstOrDefault()?.Time;
if (firstFrameTime == null || firstFrameTime <= gameplayStart + 5000) if (firstFrameTime == null || firstFrameTime <= gameplayStart + 5000)
return base.CreateGameplayClockContainer(beatmap, gameplayStart); return base.CreateGameplayClockContainer(beatmap, gameplayStart);
@ -43,6 +70,16 @@ namespace osu.Game.Screens.Play
return base.OnExiting(next); return base.OnExiting(next);
} }
private void userBeganPlaying(int userId, SpectatorState state)
{
if (userId != score.ScoreInfo.UserID) return;
Schedule(() =>
{
if (this.IsCurrentScreen()) this.Exit();
});
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
@ -50,11 +87,5 @@ namespace osu.Game.Screens.Play
if (spectatorStreaming != null) if (spectatorStreaming != null)
spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying; spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying;
} }
private void userBeganPlaying(int userId, SpectatorState state)
{
if (userId == Score.ScoreInfo.UserID)
Schedule(this.Exit);
}
} }
} }

View File

@ -0,0 +1,47 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Online.Spectator;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
namespace osu.Game.Screens.Play
{
public class SpectatorResultsScreen : SoloResultsScreen
{
public SpectatorResultsScreen(ScoreInfo score)
: base(score)
{
}
[Resolved]
private SpectatorStreamingClient spectatorStreaming { get; set; }
[BackgroundDependencyLoader]
private void load()
{
spectatorStreaming.OnUserBeganPlaying += userBeganPlaying;
}
private void userBeganPlaying(int userId, SpectatorState state)
{
if (userId == Score.UserID)
{
Schedule(() =>
{
if (this.IsCurrentScreen()) this.Exit();
});
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (spectatorStreaming != null)
spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying;
}
}
}

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using JetBrains.Annotations; using JetBrains.Annotations;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -85,6 +86,8 @@ namespace osu.Game.Screens.Select
private WorkingBeatmap beatmap; private WorkingBeatmap beatmap;
private CancellationTokenSource cancellationSource;
public WorkingBeatmap Beatmap public WorkingBeatmap Beatmap
{ {
get => beatmap; get => beatmap;
@ -93,9 +96,11 @@ namespace osu.Game.Screens.Select
if (beatmap == value) return; if (beatmap == value) return;
beatmap = value; beatmap = value;
cancellationSource?.Cancel();
cancellationSource = new CancellationTokenSource();
beatmapDifficulty?.UnbindAll(); beatmapDifficulty?.UnbindAll();
beatmapDifficulty = difficultyManager.GetBindableDifficulty(beatmap.BeatmapInfo); beatmapDifficulty = difficultyManager.GetBindableDifficulty(beatmap.BeatmapInfo, cancellationSource.Token);
beatmapDifficulty.BindValueChanged(_ => updateDisplay()); beatmapDifficulty.BindValueChanged(_ => updateDisplay());
updateDisplay(); updateDisplay();
@ -108,33 +113,44 @@ namespace osu.Game.Screens.Select
private void updateDisplay() private void updateDisplay()
{ {
void removeOldInfo() Scheduler.AddOnce(perform);
{
State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible;
Info?.FadeOut(250); void perform()
Info?.Expire(); {
Info = null; void removeOldInfo()
{
State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible;
Info?.FadeOut(250);
Info?.Expire();
Info = null;
}
if (beatmap == null)
{
removeOldInfo();
return;
}
LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, beatmapDifficulty.Value)
{
Shear = -Shear,
Depth = Info?.Depth + 1 ?? 0
}, loaded =>
{
// ensure we are the most recent loaded wedge.
if (loaded != loadingInfo) return;
removeOldInfo();
Add(Info = loaded);
});
} }
}
if (beatmap == null) protected override void Dispose(bool isDisposing)
{ {
removeOldInfo(); base.Dispose(isDisposing);
return; cancellationSource?.Cancel();
}
LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, beatmapDifficulty.Value)
{
Shear = -Shear,
Depth = Info?.Depth + 1 ?? 0
}, loaded =>
{
// ensure we are the most recent loaded wedge.
if (loaded != loadingInfo) return;
removeOldInfo();
Add(Info = loaded);
});
} }
public class BufferedWedgeInfo : BufferedContainer public class BufferedWedgeInfo : BufferedContainer

View File

@ -37,6 +37,7 @@ using osu.Framework.Input.Bindings;
using osu.Game.Collections; using osu.Game.Collections;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Scoring; using osu.Game.Scoring;
using System.Diagnostics;
namespace osu.Game.Screens.Select namespace osu.Game.Screens.Select
{ {
@ -519,7 +520,7 @@ namespace osu.Game.Screens.Select
ModSelect.SelectedMods.BindTo(selectedMods); ModSelect.SelectedMods.BindTo(selectedMods);
music.TrackChanged += ensureTrackLooping; beginLooping();
} }
private const double logo_transition = 250; private const double logo_transition = 250;
@ -570,8 +571,7 @@ namespace osu.Game.Screens.Select
BeatmapDetails.Refresh(); BeatmapDetails.Refresh();
music.CurrentTrack.Looping = true; beginLooping();
music.TrackChanged += ensureTrackLooping;
music.ResetTrackAdjustments(); music.ResetTrackAdjustments();
if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending) if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending)
@ -597,8 +597,7 @@ namespace osu.Game.Screens.Select
BeatmapOptions.Hide(); BeatmapOptions.Hide();
music.CurrentTrack.Looping = false; endLooping();
music.TrackChanged -= ensureTrackLooping;
this.ScaleTo(1.1f, 250, Easing.InSine); this.ScaleTo(1.1f, 250, Easing.InSine);
@ -619,12 +618,33 @@ namespace osu.Game.Screens.Select
FilterControl.Deactivate(); FilterControl.Deactivate();
music.CurrentTrack.Looping = false; endLooping();
music.TrackChanged -= ensureTrackLooping;
return false; return false;
} }
private bool isHandlingLooping;
private void beginLooping()
{
Debug.Assert(!isHandlingLooping);
music.CurrentTrack.Looping = isHandlingLooping = true;
music.TrackChanged += ensureTrackLooping;
}
private void endLooping()
{
// may be called multiple times during screen exit process.
if (!isHandlingLooping)
return;
music.CurrentTrack.Looping = isHandlingLooping = false;
music.TrackChanged -= ensureTrackLooping;
}
private void ensureTrackLooping(WorkingBeatmap beatmap, TrackChangeDirection changeDirection) private void ensureTrackLooping(WorkingBeatmap beatmap, TrackChangeDirection changeDirection)
=> music.CurrentTrack.Looping = true; => music.CurrentTrack.Looping = true;

View File

@ -20,6 +20,10 @@ namespace osu.Game.Users
{ {
public readonly User User; public readonly User User;
/// <summary>
/// Perform an action in addition to showing the user's profile.
/// This should be used to perform auxiliary tasks and not as a primary action for clicking a user panel (to maintain a consistent UX).
/// </summary>
public new Action Action; public new Action Action;
protected Action ViewProfile { get; private set; } protected Action ViewProfile { get; private set; }

View File

@ -73,6 +73,14 @@
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1029.1" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1029.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
<PropertyGroup>
<NoWarn>$(NoWarn);NU1605</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="3.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.3" />
</ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. --> <!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<PackageReference Include="DiffPlex" Version="1.6.3" /> <PackageReference Include="DiffPlex" Version="1.6.3" />