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

Merge branch 'master' into remove-duplicated-load

This commit is contained in:
recapitalverb 2020-02-15 12:10:34 +07:00 committed by GitHub
commit 7c9569c9a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1080 additions and 160 deletions

View File

@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using static osu.Game.Input.Handlers.ReplayInputHandler; using static osu.Game.Input.Handlers.ReplayInputHandler;
@ -24,6 +25,21 @@ namespace osu.Game.Rulesets.Osu.Mods
/// </summary> /// </summary>
private const float relax_leniency = 3; private const float relax_leniency = 3;
private bool isDownState;
private bool wasLeft;
private OsuInputManager osuInputManager;
private ReplayState<OsuAction> state;
private double lastStateChangeTime;
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
// grab the input manager for future use.
osuInputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
osuInputManager.AllowUserPresses = false;
}
public void Update(Playfield playfield) public void Update(Playfield playfield)
{ {
bool requiresHold = false; bool requiresHold = false;
@ -63,11 +79,14 @@ namespace osu.Game.Rulesets.Osu.Mods
if (requiresHit) if (requiresHit)
{ {
addAction(false); changeState(false);
addAction(true); changeState(true);
} }
addAction(requiresHold); if (requiresHold)
changeState(true);
else if (isDownState && time - lastStateChangeTime > AutoGenerator.KEY_UP_DELAY)
changeState(false);
void handleHitCircle(DrawableHitCircle circle) void handleHitCircle(DrawableHitCircle circle)
{ {
@ -77,39 +96,28 @@ namespace osu.Game.Rulesets.Osu.Mods
Debug.Assert(circle.HitObject.HitWindows != null); Debug.Assert(circle.HitObject.HitWindows != null);
requiresHit |= circle.HitObject.HitWindows.CanBeHit(time - circle.HitObject.StartTime); requiresHit |= circle.HitObject.HitWindows.CanBeHit(time - circle.HitObject.StartTime);
} }
}
private bool wasHit; void changeState(bool down)
private bool wasLeft;
private OsuInputManager osuInputManager;
private void addAction(bool hitting)
{
if (wasHit == hitting)
return;
wasHit = hitting;
var state = new ReplayState<OsuAction>
{ {
PressedActions = new List<OsuAction>() if (isDownState == down)
}; return;
if (hitting) isDownState = down;
{ lastStateChangeTime = time;
state.PressedActions.Add(wasLeft ? OsuAction.LeftButton : OsuAction.RightButton);
wasLeft = !wasLeft; state = new ReplayState<OsuAction>
{
PressedActions = new List<OsuAction>()
};
if (down)
{
state.PressedActions.Add(wasLeft ? OsuAction.LeftButton : OsuAction.RightButton);
wasLeft = !wasLeft;
}
state?.Apply(osuInputManager.CurrentState, osuInputManager);
} }
state.Apply(osuInputManager.CurrentState, osuInputManager);
}
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
// grab the input manager for future use.
osuInputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
osuInputManager.AllowUserPresses = false;
} }
} }
} }

View File

@ -91,9 +91,44 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
AddStep("load dummy beatmap", () => ResetPlayer(false)); AddStep("load dummy beatmap", () => ResetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddRepeatStep("move mouse", () => InputManager.MoveMouseTo(loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft + (loader.VisualSettings.ScreenSpaceDrawQuad.BottomRight - loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft) * RNG.NextSingle()), 20);
AddUntilStep("wait for load ready", () =>
{
moveMouse();
return player.LoadState == LoadState.Ready;
});
AddRepeatStep("move mouse", moveMouse, 20);
AddAssert("loader still active", () => loader.IsCurrentScreen()); AddAssert("loader still active", () => loader.IsCurrentScreen());
AddUntilStep("loads after idle", () => !loader.IsCurrentScreen()); AddUntilStep("loads after idle", () => !loader.IsCurrentScreen());
void moveMouse()
{
InputManager.MoveMouseTo(
loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft
+ (loader.VisualSettings.ScreenSpaceDrawQuad.BottomRight - loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft)
* RNG.NextSingle());
}
}
[Test]
public void TestBlockLoadViaFocus()
{
OsuFocusedOverlayContainer overlay = null;
AddStep("load dummy beatmap", () => ResetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddStep("show focused overlay", () => { container.Add(overlay = new ChangelogOverlay { State = { Value = Visibility.Visible } }); });
AddUntilStep("overlay visible", () => overlay.IsPresent);
AddUntilStep("wait for load ready", () => player.LoadState == LoadState.Ready);
AddRepeatStep("twiddle thumbs", () => { }, 20);
AddAssert("loader still active", () => loader.IsCurrentScreen());
AddStep("hide overlay", () => overlay.Hide());
AddUntilStep("loads after idle", () => !loader.IsCurrentScreen());
} }
[Test] [Test]
@ -159,13 +194,22 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
[Test] [Test]
public void TestMutedNotificationMasterVolume() => addVolumeSteps("master volume", () => audioManager.Volume.Value = 0, null, () => audioManager.Volume.IsDefault); public void TestMutedNotificationMasterVolume()
{
addVolumeSteps("master volume", () => audioManager.Volume.Value = 0, null, () => audioManager.Volume.IsDefault);
}
[Test] [Test]
public void TestMutedNotificationTrackVolume() => addVolumeSteps("music volume", () => audioManager.VolumeTrack.Value = 0, null, () => audioManager.VolumeTrack.IsDefault); public void TestMutedNotificationTrackVolume()
{
addVolumeSteps("music volume", () => audioManager.VolumeTrack.Value = 0, null, () => audioManager.VolumeTrack.IsDefault);
}
[Test] [Test]
public void TestMutedNotificationMuteButton() => addVolumeSteps("mute button", null, () => container.VolumeOverlay.IsMuted.Value = true, () => !container.VolumeOverlay.IsMuted.Value); public void TestMutedNotificationMuteButton()
{
addVolumeSteps("mute button", null, () => container.VolumeOverlay.IsMuted.Value = true, () => !container.VolumeOverlay.IsMuted.Value);
}
/// <remarks> /// <remarks>
/// Created for avoiding copy pasting code for the same steps. /// Created for avoiding copy pasting code for the same steps.
@ -179,7 +223,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("reset notification lock", () => sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce).Value = false); AddStep("reset notification lock", () => sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce).Value = false);
AddStep("load player", () => ResetPlayer(false, beforeLoad, afterLoad)); AddStep("load player", () => ResetPlayer(false, beforeLoad, afterLoad));
AddUntilStep("wait for player", () => player.IsLoaded); AddUntilStep("wait for player", () => player.LoadState == LoadState.Ready);
AddAssert("check for notification", () => container.NotificationOverlay.UnreadCount.Value == 1); AddAssert("check for notification", () => container.NotificationOverlay.UnreadCount.Value == 1);
AddStep("click notification", () => AddStep("click notification", () =>
@ -193,6 +237,8 @@ namespace osu.Game.Tests.Visual.Gameplay
}); });
AddAssert("check " + volumeName, assert); AddAssert("check " + volumeName, assert);
AddUntilStep("wait for player load", () => player.IsLoaded);
} }
private class TestPlayerLoaderContainer : Container private class TestPlayerLoaderContainer : Container

View File

@ -0,0 +1,230 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Multi;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneDrawableRoomPlaylist : ManualInputManagerTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(DrawableRoomPlaylist),
typeof(DrawableRoomPlaylistItem)
};
private DrawableRoomPlaylist playlist;
[Test]
public void TestNonEditableNonSelectable()
{
createPlaylist(false, false);
moveToItem(0);
assertHandleVisibility(0, false);
assertDeleteButtonVisibility(0, false);
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
}
[Test]
public void TestEditable()
{
createPlaylist(true, false);
moveToItem(0);
assertHandleVisibility(0, true);
assertDeleteButtonVisibility(0, true);
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
}
[Test]
public void TestSelectable()
{
createPlaylist(false, true);
moveToItem(0);
assertHandleVisibility(0, false);
assertDeleteButtonVisibility(0, false);
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
}
[Test]
public void TestEditableSelectable()
{
createPlaylist(true, true);
moveToItem(0);
assertHandleVisibility(0, true);
assertDeleteButtonVisibility(0, true);
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
}
[Test]
public void TestSelectionNotLostAfterRearrangement()
{
createPlaylist(true, true);
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveToDragger(0);
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
moveToDragger(1, new Vector2(0, 5));
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("item 1 is selected", () => playlist.SelectedItem.Value == playlist.Items[1]);
}
[Test]
public void TestItemRemovedOnDeletion()
{
PlaylistItem selectedItem = null;
createPlaylist(true, true);
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddStep("retrieve selection", () => selectedItem = playlist.SelectedItem.Value);
moveToDeleteButton(0);
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
AddAssert("item removed", () => !playlist.Items.Contains(selectedItem));
}
[Test]
public void TestNextItemSelectedAfterDeletion()
{
createPlaylist(true, true);
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveToDeleteButton(0);
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
}
[Test]
public void TestLastItemSelectedAfterLastItemDeleted()
{
createPlaylist(true, true);
AddWaitStep("wait for flow", 5); // Items may take 1 update frame to flow. A wait count of 5 is guaranteed to result in the flow being updated as desired.
AddStep("scroll to bottom", () => playlist.ChildrenOfType<ScrollContainer<Drawable>>().First().ScrollToEnd(false));
moveToItem(19);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveToDeleteButton(19);
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
AddAssert("item 18 is selected", () => playlist.SelectedItem.Value == playlist.Items[18]);
}
[Test]
public void TestSelectionResetWhenAllItemsDeleted()
{
createPlaylist(true, true);
AddStep("remove all but one item", () =>
{
playlist.Items.RemoveRange(1, playlist.Items.Count - 1);
});
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveToDeleteButton(0);
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
}
// Todo: currently not possible due to bindable list shortcomings (https://github.com/ppy/osu-framework/issues/3081)
// [Test]
public void TestNextItemSelectedAfterExternalDeletion()
{
createPlaylist(true, true);
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddStep("remove item 0", () => playlist.Items.RemoveAt(0));
AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
}
private void moveToItem(int index, Vector2? offset = null)
=> AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType<OsuRearrangeableListItem<PlaylistItem>>().ElementAt(index), offset));
private void moveToDragger(int index, Vector2? offset = null) => AddStep($"move mouse to dragger {index}", () =>
{
var item = playlist.ChildrenOfType<OsuRearrangeableListItem<PlaylistItem>>().ElementAt(index);
InputManager.MoveMouseTo(item.ChildrenOfType<OsuRearrangeableListItem<PlaylistItem>.PlaylistItemHandle>().Single(), offset);
});
private void moveToDeleteButton(int index, Vector2? offset = null) => AddStep($"move mouse to delete button {index}", () =>
{
var item = playlist.ChildrenOfType<OsuRearrangeableListItem<PlaylistItem>>().ElementAt(index);
InputManager.MoveMouseTo(item.ChildrenOfType<IconButton>().ElementAt(0), offset);
});
private void assertHandleVisibility(int index, bool visible)
=> AddAssert($"handle {index} {(visible ? "is" : "is not")} visible",
() => (playlist.ChildrenOfType<OsuRearrangeableListItem<PlaylistItem>.PlaylistItemHandle>().ElementAt(index).Alpha > 0) == visible);
private void assertDeleteButtonVisibility(int index, bool visible)
=> AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible", () => (playlist.ChildrenOfType<IconButton>().ElementAt(2 + index * 2).Alpha > 0) == visible);
private void createPlaylist(bool allowEdit, bool allowSelection) => AddStep("create playlist", () =>
{
Child = playlist = new DrawableRoomPlaylist(allowEdit, allowSelection)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500, 300)
};
for (int i = 0; i < 20; i++)
{
playlist.Items.Add(new PlaylistItem
{
ID = i,
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
RequiredMods =
{
new OsuModHardRock(),
new OsuModDoubleTime(),
new OsuModAutoplay()
}
});
}
});
}
}

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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Multi.Components;
using osu.Game.Tests.Beatmaps;
using osuTK;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMatchBeatmapDetailArea : MultiplayerTestScene
{
[SetUp]
public void Setup() => Schedule(() =>
{
Room.Playlist.Clear();
Child = new MatchBeatmapDetailArea
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500),
CreateNewItem = createNewItem
};
});
private void createNewItem()
{
Room.Playlist.Add(new PlaylistItem
{
ID = Room.Playlist.Count,
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
RequiredMods =
{
new OsuModHardRock(),
new OsuModDoubleTime(),
new OsuModAutoplay()
}
});
}
}
}

View File

@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Screens.Multi.Components;
using osu.Game.Screens.Select;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMatchSongSelect : MultiplayerTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(MatchSongSelect),
typeof(MatchBeatmapDetailArea),
};
[Resolved]
private BeatmapManager beatmapManager { get; set; }
[SetUp]
public void Setup() => Schedule(() =>
{
Room.Playlist.Clear();
});
[Test]
public void TestLoadSongSelect()
{
AddStep("create song select", () => LoadScreen(new MatchSongSelect()));
}
}
}

View File

@ -117,6 +117,8 @@ namespace osu.Game.Tests.Visual.Navigation
{ {
base.LoadComplete(); base.LoadComplete();
API.Login("Rhythm Champion", "osu!"); API.Login("Rhythm Champion", "osu!");
Dependencies.Get<SessionStatics>().Set(Static.MutedAudioNotificationShownOnce, true);
} }
} }

View File

@ -0,0 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Graphics.Containers
{
public abstract class OsuRearrangeableListContainer<TModel> : RearrangeableListContainer<TModel>
{
/// <summary>
/// Whether any item is currently being dragged. Used to hide other items' drag handles.
/// </summary>
private readonly BindableBool playlistDragActive = new BindableBool();
protected override ScrollContainer<Drawable> CreateScrollContainer() => new OsuScrollContainer();
protected sealed override RearrangeableListItem<TModel> CreateDrawable(TModel item) => CreateOsuDrawable(item).With(d =>
{
d.PlaylistDragActive.BindTo(playlistDragActive);
});
protected abstract OsuRearrangeableListItem<TModel> CreateOsuDrawable(TModel item);
}
}

View File

@ -0,0 +1,162 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.Containers
{
public abstract class OsuRearrangeableListItem<TModel> : RearrangeableListItem<TModel>
{
public const float FADE_DURATION = 100;
/// <summary>
/// Whether any item is currently being dragged. Used to hide other items' drag handles.
/// </summary>
public readonly BindableBool PlaylistDragActive = new BindableBool();
private Color4 handleColour = Color4.White;
/// <summary>
/// The colour of the drag handle.
/// </summary>
protected Color4 HandleColour
{
get => handleColour;
set
{
if (handleColour == value)
return;
handleColour = value;
if (handle != null)
handle.Colour = value;
}
}
/// <summary>
/// Whether the drag handle should be shown.
/// </summary>
protected virtual bool ShowDragHandle => true;
private PlaylistItemHandle handle;
protected OsuRearrangeableListItem(TModel item)
: base(item)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
}
[BackgroundDependencyLoader]
private void load()
{
Container handleContainer;
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[]
{
new[]
{
handleContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = 5 },
Child = handle = new PlaylistItemHandle
{
Size = new Vector2(12),
Colour = HandleColour,
AlwaysPresent = true,
Alpha = 0
}
},
CreateContent()
}
},
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
};
if (!ShowDragHandle)
handleContainer.Alpha = 0;
}
protected override bool OnDragStart(DragStartEvent e)
{
if (!base.OnDragStart(e))
return false;
PlaylistDragActive.Value = true;
return true;
}
protected override void OnDragEnd(DragEndEvent e)
{
PlaylistDragActive.Value = false;
base.OnDragEnd(e);
}
protected override bool IsDraggableAt(Vector2 screenSpacePos) => handle.HandlingDrag;
protected override bool OnHover(HoverEvent e)
{
handle.UpdateHoverState(IsDragged || !PlaylistDragActive.Value);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e) => handle.UpdateHoverState(false);
protected abstract Drawable CreateContent();
public class PlaylistItemHandle : SpriteIcon
{
public bool HandlingDrag { get; private set; }
private bool isHovering;
public PlaylistItemHandle()
{
Icon = FontAwesome.Solid.Bars;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
base.OnMouseDown(e);
HandlingDrag = true;
UpdateHoverState(isHovering);
return false;
}
protected override void OnMouseUp(MouseUpEvent e)
{
base.OnMouseUp(e);
HandlingDrag = false;
UpdateHoverState(isHovering);
}
public void UpdateHoverState(bool hovering)
{
isHovering = hovering;
if (isHovering || HandlingDrag)
this.FadeIn(FADE_DURATION);
else
this.FadeOut(FADE_DURATION);
}
}
}
}

View File

@ -45,7 +45,7 @@ namespace osu.Game.Overlays.Direct
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(OsuGame game, BeatmapManager beatmaps) private void load(OsuGame game, BeatmapManager beatmaps)
{ {
if (BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false) if (BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false)
{ {
button.Enabled.Value = false; button.Enabled.Value = false;
button.TooltipText = "this beatmap is currently not available for download."; button.TooltipText = "this beatmap is currently not available for download.";
@ -62,7 +62,7 @@ namespace osu.Game.Overlays.Direct
break; break;
case DownloadState.LocallyAvailable: case DownloadState.LocallyAvailable:
game.PresentBeatmap(BeatmapSet.Value); game?.PresentBeatmap(BeatmapSet.Value);
break; break;
default: default:

View File

@ -12,17 +12,12 @@ using osuTK;
namespace osu.Game.Overlays.Music namespace osu.Game.Overlays.Music
{ {
public class Playlist : RearrangeableListContainer<BeatmapSetInfo> public class Playlist : OsuRearrangeableListContainer<BeatmapSetInfo>
{ {
public Action<BeatmapSetInfo> RequestSelection; public Action<BeatmapSetInfo> RequestSelection;
public readonly Bindable<BeatmapSetInfo> SelectedSet = new Bindable<BeatmapSetInfo>(); public readonly Bindable<BeatmapSetInfo> SelectedSet = new Bindable<BeatmapSetInfo>();
/// <summary>
/// Whether any item is currently being dragged. Used to hide other items' drag handles.
/// </summary>
private readonly BindableBool playlistDragActive = new BindableBool();
public new MarginPadding Padding public new MarginPadding Padding
{ {
get => base.Padding; get => base.Padding;
@ -33,15 +28,12 @@ namespace osu.Game.Overlays.Music
public BeatmapSetInfo FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter); public BeatmapSetInfo FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter);
protected override RearrangeableListItem<BeatmapSetInfo> CreateDrawable(BeatmapSetInfo item) => new PlaylistItem(item) protected override OsuRearrangeableListItem<BeatmapSetInfo> CreateOsuDrawable(BeatmapSetInfo item) => new PlaylistItem(item)
{ {
SelectedSet = { BindTarget = SelectedSet }, SelectedSet = { BindTarget = SelectedSet },
PlaylistDragActive = { BindTarget = playlistDragActive },
RequestSelection = set => RequestSelection?.Invoke(set) RequestSelection = set => RequestSelection?.Invoke(set)
}; };
protected override ScrollContainer<Drawable> CreateScrollContainer() => new OsuScrollContainer();
protected override FillFlowContainer<RearrangeableListItem<BeatmapSetInfo>> CreateListFillFlowContainer() => new SearchContainer<RearrangeableListItem<BeatmapSetInfo>> protected override FillFlowContainer<RearrangeableListItem<BeatmapSetInfo>> CreateListFillFlowContainer() => new SearchContainer<RearrangeableListItem<BeatmapSetInfo>>
{ {
Spacing = new Vector2(0, 3), Spacing = new Vector2(0, 3),

View File

@ -14,36 +14,27 @@ using osu.Framework.Localisation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Overlays.Music namespace osu.Game.Overlays.Music
{ {
public class PlaylistItem : RearrangeableListItem<BeatmapSetInfo>, IFilterable public class PlaylistItem : OsuRearrangeableListItem<BeatmapSetInfo>, IFilterable
{ {
private const float fade_duration = 100;
public BindableBool PlaylistDragActive = new BindableBool();
public readonly Bindable<BeatmapSetInfo> SelectedSet = new Bindable<BeatmapSetInfo>(); public readonly Bindable<BeatmapSetInfo> SelectedSet = new Bindable<BeatmapSetInfo>();
public Action<BeatmapSetInfo> RequestSelection; public Action<BeatmapSetInfo> RequestSelection;
private PlaylistItemHandle handle;
private TextFlowContainer text; private TextFlowContainer text;
private IEnumerable<Drawable> titleSprites; private IEnumerable<Drawable> titleSprites;
private ILocalisedBindableString titleBind; private ILocalisedBindableString titleBind;
private ILocalisedBindableString artistBind; private ILocalisedBindableString artistBind;
private Color4 hoverColour; private Color4 selectedColour;
private Color4 artistColour; private Color4 artistColour;
public PlaylistItem(BeatmapSetInfo item) public PlaylistItem(BeatmapSetInfo item)
: base(item) : base(item)
{ {
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding { Left = 5 }; Padding = new MarginPadding { Left = 5 };
FilterTerms = item.Metadata.SearchableTerms; FilterTerms = item.Metadata.SearchableTerms;
@ -52,42 +43,12 @@ namespace osu.Game.Overlays.Music
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours, LocalisationManager localisation) private void load(OsuColour colours, LocalisationManager localisation)
{ {
hoverColour = colours.Yellow; selectedColour = colours.Yellow;
artistColour = colours.Gray9; artistColour = colours.Gray9;
HandleColour = colours.Gray5;
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[]
{
new Drawable[]
{
handle = new PlaylistItemHandle
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(12),
Colour = colours.Gray5,
AlwaysPresent = true,
Alpha = 0
},
text = new OsuTextFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = 5 },
},
}
},
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
};
titleBind = localisation.GetLocalisedString(new LocalisedString((Model.Metadata.TitleUnicode, Model.Metadata.Title))); titleBind = localisation.GetLocalisedString(new LocalisedString((Model.Metadata.TitleUnicode, Model.Metadata.Title)));
artistBind = localisation.GetLocalisedString(new LocalisedString((Model.Metadata.ArtistUnicode, Model.Metadata.Artist))); artistBind = localisation.GetLocalisedString(new LocalisedString((Model.Metadata.ArtistUnicode, Model.Metadata.Artist)));
artistBind.BindValueChanged(_ => recreateText(), true);
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -100,10 +61,18 @@ namespace osu.Game.Overlays.Music
return; return;
foreach (Drawable s in titleSprites) foreach (Drawable s in titleSprites)
s.FadeColour(set.NewValue == Model ? hoverColour : Color4.White, fade_duration); s.FadeColour(set.NewValue == Model ? selectedColour : Color4.White, FADE_DURATION);
}, true); }, true);
artistBind.BindValueChanged(_ => recreateText(), true);
} }
protected override Drawable CreateContent() => text = new OsuTextFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
};
private void recreateText() private void recreateText()
{ {
text.Clear(); text.Clear();
@ -125,31 +94,6 @@ namespace osu.Game.Overlays.Music
return true; return true;
} }
protected override bool OnDragStart(DragStartEvent e)
{
if (!base.OnDragStart(e))
return false;
PlaylistDragActive.Value = true;
return true;
}
protected override void OnDragEnd(DragEndEvent e)
{
PlaylistDragActive.Value = false;
base.OnDragEnd(e);
}
protected override bool IsDraggableAt(Vector2 screenSpacePos) => handle.HandlingDrag;
protected override bool OnHover(HoverEvent e)
{
handle.UpdateHoverState(IsDragged || !PlaylistDragActive.Value);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e) => handle.UpdateHoverState(false);
public IEnumerable<string> FilterTerms { get; } public IEnumerable<string> FilterTerms { get; }
private bool matching = true; private bool matching = true;
@ -168,44 +112,5 @@ namespace osu.Game.Overlays.Music
} }
public bool FilteringActive { get; set; } public bool FilteringActive { get; set; }
private class PlaylistItemHandle : SpriteIcon
{
public bool HandlingDrag { get; private set; }
private bool isHovering;
public PlaylistItemHandle()
{
Icon = FontAwesome.Solid.Bars;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
base.OnMouseDown(e);
HandlingDrag = true;
UpdateHoverState(isHovering);
return false;
}
protected override void OnMouseUp(MouseUpEvent e)
{
base.OnMouseUp(e);
HandlingDrag = false;
UpdateHoverState(isHovering);
}
public void UpdateHoverState(bool hovering)
{
isHovering = hovering;
if (isHovering || HandlingDrag)
this.FadeIn(fade_duration);
else
this.FadeOut(fade_duration);
}
}
} }
} }

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Replays
#region Constants #region Constants
// Shared amongst all modes // Shared amongst all modes
protected const double KEY_UP_DELAY = 50; public const double KEY_UP_DELAY = 50;
#endregion #endregion

View File

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

View File

@ -0,0 +1,98 @@
// 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.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Select;
using osuTK;
namespace osu.Game.Screens.Multi.Components
{
public class MatchBeatmapDetailArea : BeatmapDetailArea
{
public Action CreateNewItem;
public readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
[Resolved(typeof(Room))]
protected BindableList<PlaylistItem> Playlist { get; private set; }
private readonly Drawable playlistArea;
private readonly DrawableRoomPlaylist playlist;
public MatchBeatmapDetailArea()
{
Add(playlistArea = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Vertical = 10 },
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = 10 },
Child = playlist = new DrawableRoomPlaylist(true, false)
{
RelativeSizeAxes = Axes.Both,
}
}
},
new Drawable[]
{
new TriangleButton
{
Text = "create new item",
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
Action = () => CreateNewItem?.Invoke()
}
},
},
RowDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.Absolute, 50),
}
}
});
}
protected override void LoadComplete()
{
base.LoadComplete();
playlist.Items.BindTo(Playlist);
playlist.SelectedItem.BindTo(SelectedItem);
}
protected override void OnTabChanged(BeatmapDetailAreaTabItem tab, bool selectedMods)
{
base.OnTabChanged(tab, selectedMods);
switch (tab)
{
case BeatmapDetailAreaPlaylistTabItem _:
playlistArea.Show();
break;
default:
playlistArea.Hide();
break;
}
}
protected override BeatmapDetailAreaTabItem[] CreateTabItems() => base.CreateTabItems().Prepend(new BeatmapDetailAreaPlaylistTabItem()).ToArray();
}
}

View File

@ -0,0 +1,72 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
using osu.Game.Online.Multiplayer;
using osuTK;
namespace osu.Game.Screens.Multi
{
public class DrawableRoomPlaylist : OsuRearrangeableListContainer<PlaylistItem>
{
public readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
private readonly bool allowEdit;
private readonly bool allowSelection;
public DrawableRoomPlaylist(bool allowEdit, bool allowSelection)
{
this.allowEdit = allowEdit;
this.allowSelection = allowSelection;
}
protected override void LoadComplete()
{
base.LoadComplete();
// Scheduled since items are removed and re-added upon rearrangement
Items.ItemsRemoved += items => Schedule(() =>
{
if (!Items.Contains(SelectedItem.Value))
SelectedItem.Value = null;
});
}
protected override ScrollContainer<Drawable> CreateScrollContainer() => base.CreateScrollContainer().With(d =>
{
d.ScrollbarVisible = false;
});
protected override FillFlowContainer<RearrangeableListItem<PlaylistItem>> CreateListFillFlowContainer() => new FillFlowContainer<RearrangeableListItem<PlaylistItem>>
{
LayoutDuration = 200,
LayoutEasing = Easing.OutQuint,
Spacing = new Vector2(0, 2)
};
protected override OsuRearrangeableListItem<PlaylistItem> CreateOsuDrawable(PlaylistItem item) => new DrawableRoomPlaylistItem(item, allowEdit, allowSelection)
{
SelectedItem = { BindTarget = SelectedItem },
RequestDeletion = requestDeletion
};
private void requestSelection(PlaylistItem item) => SelectedItem.Value = item;
private void requestDeletion(PlaylistItem item)
{
if (SelectedItem.Value == item)
{
if (Items.Count == 1)
SelectedItem.Value = null;
else
SelectedItem.Value = Items.GetNext(item) ?? Items[^2];
}
Items.Remove(item);
}
}
}

View File

@ -0,0 +1,275 @@
// 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.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Chat;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays.Direct;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play.HUD;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Multi
{
public class DrawableRoomPlaylistItem : OsuRearrangeableListItem<PlaylistItem>
{
public Action<PlaylistItem> RequestDeletion;
public readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
protected override bool ShowDragHandle => allowEdit;
private Container maskingContainer;
private Container difficultyIconContainer;
private LinkFlowContainer beatmapText;
private LinkFlowContainer authorText;
private ModDisplay modDisplay;
private readonly Bindable<BeatmapInfo> beatmap = new Bindable<BeatmapInfo>();
private readonly Bindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
private readonly BindableList<Mod> requiredMods = new BindableList<Mod>();
private readonly PlaylistItem item;
private readonly bool allowEdit;
private readonly bool allowSelection;
public DrawableRoomPlaylistItem(PlaylistItem item, bool allowEdit, bool allowSelection)
: base(item)
{
this.item = item;
this.allowEdit = allowEdit;
this.allowSelection = allowSelection;
beatmap.BindTo(item.Beatmap);
ruleset.BindTo(item.Ruleset);
requiredMods.BindTo(item.RequiredMods);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
if (!allowEdit)
HandleColour = HandleColour.Opacity(0);
maskingContainer.BorderColour = colours.Yellow;
}
protected override void LoadComplete()
{
base.LoadComplete();
SelectedItem.BindValueChanged(selected => maskingContainer.BorderThickness = selected.NewValue == Model ? 5 : 0, true);
beatmap.BindValueChanged(_ => scheduleRefresh());
ruleset.BindValueChanged(_ => scheduleRefresh());
requiredMods.ItemsAdded += _ => scheduleRefresh();
requiredMods.ItemsRemoved += _ => scheduleRefresh();
refresh();
}
private ScheduledDelegate scheduledRefresh;
private void scheduleRefresh()
{
scheduledRefresh?.Cancel();
scheduledRefresh = Schedule(refresh);
}
private void refresh()
{
difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value) { Size = new Vector2(32) };
beatmapText.Clear();
beatmapText.AddLink(item.Beatmap.ToString(), LinkAction.OpenBeatmap, item.Beatmap.Value.OnlineBeatmapID.ToString());
authorText.Clear();
if (item.Beatmap?.Value?.Metadata?.Author != null)
{
authorText.AddText("mapped by ");
authorText.AddUserLink(item.Beatmap.Value?.Metadata.Author);
}
modDisplay.Current.Value = requiredMods.ToArray();
}
protected override Drawable CreateContent() => maskingContainer = new Container
{
RelativeSizeAxes = Axes.X,
Height = 50,
Masking = true,
CornerRadius = 10,
Children = new Drawable[]
{
new Box // A transparent box that forces the border to be drawn if the panel background is opaque
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
},
new PanelBackground
{
RelativeSizeAxes = Axes.Both,
Beatmap = { BindTarget = beatmap }
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 8 },
Spacing = new Vector2(8, 0),
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
difficultyIconContainer = new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
},
new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
beatmapText = new LinkFlowContainer { AutoSizeAxes = Axes.Both },
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
authorText = new LinkFlowContainer { AutoSizeAxes = Axes.Both },
modDisplay = new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Scale = new Vector2(0.4f),
DisplayUnrankedText = false,
ExpansionMode = ExpansionMode.AlwaysExpanded
}
}
}
}
}
}
},
new Container
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
X = -18,
Children = new Drawable[]
{
new IconButton
{
Icon = FontAwesome.Solid.MinusSquare,
Alpha = allowEdit ? 1 : 0,
Action = () => RequestDeletion?.Invoke(Model),
},
new PanelDownloadButton(item.Beatmap.Value.BeatmapSet)
{
Size = new Vector2(50, 30),
Alpha = allowEdit ? 0 : 1
}
}
}
}
};
protected override bool OnClick(ClickEvent e)
{
if (allowSelection)
SelectedItem.Value = Model;
return true;
}
// For now, this is the same implementation as in PanelBackground, but supports a beatmap info rather than a working beatmap
private class PanelBackground : Container // todo: should be a buffered container (https://github.com/ppy/osu-framework/issues/3222)
{
public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
public PanelBackground()
{
InternalChildren = new Drawable[]
{
new UpdateableBeatmapBackgroundSprite
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
Beatmap = { BindTarget = Beatmap }
},
new Container
{
Depth = -1,
RelativeSizeAxes = Axes.Both,
// This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle
Shear = new Vector2(0.8f, 0),
Alpha = 0.5f,
Children = new[]
{
// The left half with no gradient applied
new Box
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
Colour = Color4.Black,
Width = 0.4f,
},
// Piecewise-linear gradient with 3 segments to make it appear smoother
new Box
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.9f)),
Width = 0.05f,
X = 0.4f,
},
new Box
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.9f), new Color4(0f, 0f, 0f, 0.1f)),
Width = 0.2f,
X = 0.45f,
},
new Box
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)),
Width = 0.05f,
X = 0.65f,
},
}
}
};
}
}
}
}

View File

@ -67,7 +67,14 @@ namespace osu.Game.Screens.Play
} }
private bool readyForPush => private bool readyForPush =>
player.LoadState == LoadState.Ready && (IsHovered || idleTracker.IsIdle.Value) && inputManager?.DraggedDrawable == null; // don't push unless the player is completely loaded
player.LoadState == LoadState.Ready
// don't push if the user is hovering one of the panes, unless they are idle.
&& (IsHovered || idleTracker.IsIdle.Value)
// don't push if the user is dragging a slider or otherwise.
&& inputManager?.DraggedDrawable == null
// don't push if a focused overlay is visible, like settings.
&& inputManager?.FocusedDrawable == null;
private readonly Func<Player> createPlayer; private readonly Func<Player> createPlayer;

View File

@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Multi; using osu.Game.Screens.Multi;
using osu.Game.Screens.Multi.Components;
namespace osu.Game.Screens.Select namespace osu.Game.Screens.Select
{ {
@ -35,7 +36,7 @@ namespace osu.Game.Screens.Select
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING };
} }
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); // Todo: Temporary protected override BeatmapDetailArea CreateBeatmapDetailArea() => new MatchBeatmapDetailArea();
protected override bool OnStart() protected override bool OnStart()
{ {