1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-13 02:32:55 +08:00

Merge branch 'master' into remove-hud-component-lookup

This commit is contained in:
Salman Ahmed 2021-05-31 09:23:04 +03:00
commit 02d18c7a49
244 changed files with 7045 additions and 3215 deletions

View File

@ -31,6 +31,12 @@
"commands": [ "commands": [
"CodeFileSanity" "CodeFileSanity"
] ]
},
"ppy.localisationanalyser.tools": {
"version": "2021.524.0",
"commands": [
"localisation"
]
} }
} }
} }

View File

@ -1,7 +0,0 @@
---
name: Feature Request
about: Propose a feature you would like to see in the game!
---
**Describe the new feature:**
**Proposal designs of the feature:**

View File

@ -1,5 +1,12 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Suggestions or feature request
url: https://github.com/ppy/osu/discussions/categories/ideas
about: Got something you think should change or be added? Search for or start a new discussion!
- name: Help
url: https://github.com/ppy/osu/discussions/categories/q-a
about: osu! not working as you'd expect? Not sure it's a bug? Check the Q&A section!
- name: osu!stable issues - name: osu!stable issues
url: https://github.com/ppy/osu-stable-issues url: https://github.com/ppy/osu-stable-issues
about: For issues regarding osu!stable (not osu!lazer), open them here. about: For osu!stable bugs (not osu!lazer), check out the dedicated repository. Note that we only accept serious bug reports.

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.513.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.528.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -161,13 +161,13 @@ namespace osu.Game.Rulesets.Catch
switch (result) switch (result)
{ {
case HitResult.LargeTickHit: case HitResult.LargeTickHit:
return "large droplet"; return "Large droplet";
case HitResult.SmallTickHit: case HitResult.SmallTickHit:
return "small droplet"; return "Small droplet";
case HitResult.LargeBonus: case HitResult.LargeBonus:
return "banana"; return "Banana";
} }
return base.GetDisplayNameForHitResult(result); return base.GetDisplayNameForHitResult(result);

View File

@ -22,28 +22,27 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
public override Drawable GetDrawableComponent(ISkinComponent component) public override Drawable GetDrawableComponent(ISkinComponent component)
{ {
if (!(component is CatchSkinComponent catchSkinComponent)) if (component is CatchSkinComponent catchSkinComponent)
return null; {
switch (catchSkinComponent.Component) switch (catchSkinComponent.Component)
{ {
case CatchSkinComponents.Fruit: case CatchSkinComponents.Fruit:
if (GetTexture("fruit-pear") != null) if (GetTexture("fruit-pear") != null)
return new LegacyFruitPiece(); return new LegacyFruitPiece();
break; return null;
case CatchSkinComponents.Banana: case CatchSkinComponents.Banana:
if (GetTexture("fruit-bananas") != null) if (GetTexture("fruit-bananas") != null)
return new LegacyBananaPiece(); return new LegacyBananaPiece();
break; return null;
case CatchSkinComponents.Droplet: case CatchSkinComponents.Droplet:
if (GetTexture("fruit-drop") != null) if (GetTexture("fruit-drop") != null)
return new LegacyDropletPiece(); return new LegacyDropletPiece();
break; return null;
case CatchSkinComponents.CatcherIdle: case CatchSkinComponents.CatcherIdle:
return this.GetAnimation("fruit-catcher-idle", true, true, true) ?? return this.GetAnimation("fruit-catcher-idle", true, true, true) ??
@ -61,10 +60,11 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
if (providesComboCounter) if (providesComboCounter)
return new LegacyCatchComboCounter(Source); return new LegacyCatchComboCounter(Source);
break; return null;
}
} }
return null; return Source.GetDrawableComponent(component);
} }
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)

View File

@ -15,7 +15,6 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests.Editor namespace osu.Game.Rulesets.Mania.Tests.Editor
@ -35,7 +34,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Test] [Test]
public void TestPlaceBeforeCurrentTimeDownwards() public void TestPlaceBeforeCurrentTimeDownwards()
{ {
AddStep("move mouse before current time", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Single().ScreenSpaceDrawQuad.BottomLeft - new Vector2(0, 10))); AddStep("move mouse before current time", () =>
{
var column = this.ChildrenOfType<Column>().Single();
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(-100));
});
AddStep("click", () => InputManager.Click(MouseButton.Left)); AddStep("click", () => InputManager.Click(MouseButton.Left));
@ -45,7 +48,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Test] [Test]
public void TestPlaceAfterCurrentTimeDownwards() public void TestPlaceAfterCurrentTimeDownwards()
{ {
AddStep("move mouse after current time", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Single())); AddStep("move mouse after current time", () =>
{
var column = this.ChildrenOfType<Column>().Single();
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(100));
});
AddStep("click", () => InputManager.Click(MouseButton.Left)); AddStep("click", () => InputManager.Click(MouseButton.Left));

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Width = 0.5f, Width = 0.5f,
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 0), _ => new DefaultColumnBackground()) Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
} }
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Width = 0.5f, Width = 0.5f,
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 1), _ => new DefaultColumnBackground()) Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
} }

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Width = 0.5f, Width = 0.5f,
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, 0), _ => new DefaultKeyArea()) Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Width = 0.5f, Width = 0.5f,
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, 1), _ => new DefaultKeyArea()) Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },

View File

@ -0,0 +1,67 @@
// 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.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests
{
public class TestSceneDrawableManiaHitObject : OsuTestScene
{
private readonly ManualClock clock = new ManualClock();
private Column column;
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = new ScrollingTestContainer(ScrollingDirection.Down)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
TimeRange = 2000,
Clock = new FramedClock(clock),
Child = column = new Column(0)
{
Action = { Value = ManiaAction.Key1 },
Height = 0.85f,
AccentColour = Color4.Gray
},
};
});
[Test]
public void TestHoldNoteHeadVisibility()
{
DrawableHoldNote note = null;
AddStep("Add hold note", () =>
{
var h = new HoldNote
{
StartTime = 0,
Duration = 1000
};
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
column.Add(note = new DrawableHoldNote(h));
});
AddStep("Hold key", () =>
{
clock.CurrentTime = 0;
note.OnPressed(ManiaAction.Key1);
});
AddStep("progress time", () => clock.CurrentTime = 500);
AddAssert("head is visible", () => note.Head.Alpha == 1);
}
}
}

View File

@ -5,13 +5,11 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -414,14 +412,7 @@ namespace osu.Game.Rulesets.Mania.Tests
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
AddUntilStep("wait for head", () => currentPlayer.GameplayClockContainer.GameplayClock.CurrentTime >= time_head); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor?.HasCompleted.Value == true);
AddAssert("head is visible",
() => currentPlayer.ChildrenOfType<DrawableHoldNote>()
.Single(note => note.HitObject == beatmap.HitObjects[0])
.Head
.Alpha == 1);
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
} }
private class ScoreAccessibleReplayPlayer : ReplayPlayer private class ScoreAccessibleReplayPlayer : ReplayPlayer

View File

@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.Edit
foreach (var line in grid.Objects.OfType<DrawableGridLine>()) foreach (var line in grid.Objects.OfType<DrawableGridLine>())
availableLines.Push(line); availableLines.Push(line);
grid.Clear(false); grid.Clear();
} }
if (selectionTimeRange == null) if (selectionTimeRange == null)

View File

@ -58,8 +58,9 @@ namespace osu.Game.Rulesets.Mania.Edit
EditorBeatmap.PerformOnSelection(h => EditorBeatmap.PerformOnSelection(h =>
{ {
if (h is ManiaHitObject maniaObj) maniaPlayfield.Remove(h);
maniaObj.Column += columnDelta; ((ManiaHitObject)h).Column += columnDelta;
maniaPlayfield.Add(h);
}); });
} }
} }

View File

@ -9,12 +9,6 @@ namespace osu.Game.Rulesets.Mania
{ {
public class ManiaSkinComponent : GameplaySkinComponent<ManiaSkinComponents> public class ManiaSkinComponent : GameplaySkinComponent<ManiaSkinComponents>
{ {
/// <summary>
/// The intended <see cref="Column"/> index for this component.
/// May be null if the component does not exist in a <see cref="Column"/>.
/// </summary>
public readonly int? TargetColumn;
/// <summary> /// <summary>
/// The intended <see cref="StageDefinition"/> for this component. /// The intended <see cref="StageDefinition"/> for this component.
/// May be null if the component is not a direct member of a <see cref="Stage"/>. /// May be null if the component is not a direct member of a <see cref="Stage"/>.
@ -25,12 +19,10 @@ namespace osu.Game.Rulesets.Mania
/// Creates a new <see cref="ManiaSkinComponent"/>. /// Creates a new <see cref="ManiaSkinComponent"/>.
/// </summary> /// </summary>
/// <param name="component">The component.</param> /// <param name="component">The component.</param>
/// <param name="targetColumn">The intended <see cref="Column"/> index for this component. May be null if the component does not exist in a <see cref="Column"/>.</param>
/// <param name="stageDefinition">The intended <see cref="StageDefinition"/> for this component. May be null if the component is not a direct member of a <see cref="Stage"/>.</param> /// <param name="stageDefinition">The intended <see cref="StageDefinition"/> for this component. May be null if the component is not a direct member of a <see cref="Stage"/>.</param>
public ManiaSkinComponent(ManiaSkinComponents component, int? targetColumn = null, StageDefinition? stageDefinition = null) public ManiaSkinComponent(ManiaSkinComponents component, StageDefinition? stageDefinition = null)
: base(component) : base(component)
{ {
TargetColumn = targetColumn;
StageDefinition = stageDefinition; StageDefinition = stageDefinition;
} }

View File

@ -2,6 +2,7 @@
// 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 System;
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;
@ -12,6 +13,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Mania.Objects.Drawables namespace osu.Game.Rulesets.Mania.Objects.Drawables
{ {
@ -29,21 +31,21 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public DrawableHoldNoteHead Head => headContainer.Child; public DrawableHoldNoteHead Head => headContainer.Child;
public DrawableHoldNoteTail Tail => tailContainer.Child; public DrawableHoldNoteTail Tail => tailContainer.Child;
private readonly Container<DrawableHoldNoteHead> headContainer; private Container<DrawableHoldNoteHead> headContainer;
private readonly Container<DrawableHoldNoteTail> tailContainer; private Container<DrawableHoldNoteTail> tailContainer;
private readonly Container<DrawableHoldNoteTick> tickContainer; private Container<DrawableHoldNoteTick> tickContainer;
/// <summary> /// <summary>
/// Contains the size of the hold note covering the whole head/tail bounds. The size of this container changes as the hold note is being pressed. /// Contains the size of the hold note covering the whole head/tail bounds. The size of this container changes as the hold note is being pressed.
/// </summary> /// </summary>
private readonly Container sizingContainer; private Container sizingContainer;
/// <summary> /// <summary>
/// Contains the contents of the hold note that should be masked as the hold note is being pressed. Follows changes in the size of <see cref="sizingContainer"/>. /// Contains the contents of the hold note that should be masked as the hold note is being pressed. Follows changes in the size of <see cref="sizingContainer"/>.
/// </summary> /// </summary>
private readonly Container maskingContainer; private Container maskingContainer;
private readonly SkinnableDrawable bodyPiece; private SkinnableDrawable bodyPiece;
/// <summary> /// <summary>
/// Time at which the user started holding this hold note. Null if the user is not holding this hold note. /// Time at which the user started holding this hold note. Null if the user is not holding this hold note.
@ -60,11 +62,19 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary> /// </summary>
private double? releaseTime; private double? releaseTime;
public DrawableHoldNote()
: this(null)
{
}
public DrawableHoldNote(HoldNote hitObject) public DrawableHoldNote(HoldNote hitObject)
: base(hitObject) : base(hitObject)
{ {
RelativeSizeAxes = Axes.X; }
[BackgroundDependencyLoader]
private void load()
{
Container maskedContents; Container maskedContents;
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
@ -86,7 +96,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
headContainer = new Container<DrawableHoldNoteHead> { RelativeSizeAxes = Axes.Both } headContainer = new Container<DrawableHoldNoteHead> { RelativeSizeAxes = Axes.Both }
} }
}, },
bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody), _ => new DefaultBodyPiece
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}) })
@ -105,6 +115,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
}); });
} }
protected override void OnApply()
{
base.OnApply();
sizingContainer.Size = Vector2.One;
HoldStartTime = null;
HoldBrokenTime = null;
releaseTime = null;
}
protected override void AddNestedHitObject(DrawableHitObject hitObject) protected override void AddNestedHitObject(DrawableHitObject hitObject)
{ {
base.AddNestedHitObject(hitObject); base.AddNestedHitObject(hitObject);
@ -128,37 +148,23 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected override void ClearNestedHitObjects() protected override void ClearNestedHitObjects()
{ {
base.ClearNestedHitObjects(); base.ClearNestedHitObjects();
headContainer.Clear(); headContainer.Clear(false);
tailContainer.Clear(); tailContainer.Clear(false);
tickContainer.Clear(); tickContainer.Clear(false);
} }
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
{ {
switch (hitObject) switch (hitObject)
{ {
case TailNote _: case TailNote tail:
return new DrawableHoldNoteTail(this) return new DrawableHoldNoteTail(tail);
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AccentColour = { BindTarget = AccentColour }
};
case Note _: case HeadNote head:
return new DrawableHoldNoteHead(this) return new DrawableHoldNoteHead(head);
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AccentColour = { BindTarget = AccentColour }
};
case HoldNoteTick tick: case HoldNoteTick tick:
return new DrawableHoldNoteTick(tick) return new DrawableHoldNoteTick(tick);
{
HoldStartTime = () => HoldStartTime,
AccentColour = { BindTarget = AccentColour }
};
} }
return base.CreateNestedHitObject(hitObject); return base.CreateNestedHitObject(hitObject);

View File

@ -1,6 +1,7 @@
// 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 osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Objects.Drawables namespace osu.Game.Rulesets.Mania.Objects.Drawables
@ -12,11 +13,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{ {
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteHead; protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteHead;
public DrawableHoldNoteHead(DrawableHoldNote holdNote) public DrawableHoldNoteHead()
: base(holdNote.HitObject.Head) : this(null)
{ {
} }
public DrawableHoldNoteHead(HeadNote headNote)
: base(headNote)
{
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
}
public void UpdateResult() => base.UpdateResult(true); public void UpdateResult() => base.UpdateResult(true);
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()

View File

@ -2,6 +2,7 @@
// 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.Diagnostics; using System.Diagnostics;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects.Drawables namespace osu.Game.Rulesets.Mania.Objects.Drawables
@ -20,12 +21,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail; protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail;
private readonly DrawableHoldNote holdNote; protected DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject;
public DrawableHoldNoteTail(DrawableHoldNote holdNote) public DrawableHoldNoteTail()
: base(holdNote.HitObject.Tail) : this(null)
{ {
this.holdNote = holdNote; }
public DrawableHoldNoteTail(TailNote tailNote)
: base(tailNote)
{
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
} }
public void UpdateResult() => base.UpdateResult(true); public void UpdateResult() => base.UpdateResult(true);
@ -54,7 +61,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
ApplyResult(r => ApplyResult(r =>
{ {
// If the head wasn't hit or the hold note was broken, cap the max score to Meh. // If the head wasn't hit or the hold note was broken, cap the max score to Meh.
if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HoldBrokenTime != null)) if (result > HitResult.Meh && (!HoldNote.Head.IsHit || HoldNote.HoldBrokenTime != null))
result = HitResult.Meh; result = HitResult.Meh;
r.Type = result; r.Type = result;

View File

@ -2,7 +2,8 @@
// 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 System;
using osuTK; using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -19,22 +20,28 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// <summary> /// <summary>
/// References the time at which the user started holding the hold note. /// References the time at which the user started holding the hold note.
/// </summary> /// </summary>
public Func<double?> HoldStartTime; private Func<double?> holdStartTime;
private Container glowContainer;
public DrawableHoldNoteTick()
: this(null)
{
}
public DrawableHoldNoteTick(HoldNoteTick hitObject) public DrawableHoldNoteTick(HoldNoteTick hitObject)
: base(hitObject) : base(hitObject)
{ {
Container glowContainer;
Anchor = Anchor.TopCentre; Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre; Origin = Anchor.TopCentre;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Size = new Vector2(1); }
AddRangeInternal(new[] [BackgroundDependencyLoader]
private void load()
{ {
glowContainer = new CircularContainer AddInternal(glowContainer = new CircularContainer
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
@ -49,8 +56,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
AlwaysPresent = true AlwaysPresent = true
} }
} }
}
}); });
}
protected override void LoadComplete()
{
base.LoadComplete();
AccentColour.BindValueChanged(colour => AccentColour.BindValueChanged(colour =>
{ {
@ -64,12 +75,29 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
}, true); }, true);
} }
protected override void OnApply()
{
base.OnApply();
Debug.Assert(ParentHitObject != null);
var holdNote = (DrawableHoldNote)ParentHitObject;
holdStartTime = () => holdNote.HoldStartTime;
}
protected override void OnFree()
{
base.OnFree();
holdStartTime = null;
}
protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void CheckForResult(bool userTriggered, double timeOffset)
{ {
if (Time.Current < HitObject.StartTime) if (Time.Current < HitObject.StartTime)
return; return;
var startTime = HoldStartTime?.Invoke(); var startTime = holdStartTime?.Invoke();
if (startTime == null || startTime > HitObject.StartTime) if (startTime == null || startTime > HitObject.StartTime)
ApplyResult(r => r.Type = r.Judgement.MinResult); ApplyResult(r => r.Type = r.Judgement.MinResult);

View File

@ -50,6 +50,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected DrawableManiaHitObject(ManiaHitObject hitObject) protected DrawableManiaHitObject(ManiaHitObject hitObject)
: base(hitObject) : base(hitObject)
{ {
RelativeSizeAxes = Axes.X;
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
@ -59,9 +60,31 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Action.BindTo(action); Action.BindTo(action);
Direction.BindTo(scrollingInfo.Direction); Direction.BindTo(scrollingInfo.Direction);
}
protected override void LoadComplete()
{
base.LoadComplete();
Direction.BindValueChanged(OnDirectionChanged, true); Direction.BindValueChanged(OnDirectionChanged, true);
} }
protected override void OnApply()
{
base.OnApply();
if (ParentHitObject != null)
AccentColour.BindTo(ParentHitObject.AccentColour);
}
protected override void OnFree()
{
base.OnFree();
if (ParentHitObject != null)
AccentColour.UnbindFrom(ParentHitObject.AccentColour);
}
private double computedLifetimeStart; private double computedLifetimeStart;
public override double LifetimeStart public override double LifetimeStart
@ -147,12 +170,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public abstract class DrawableManiaHitObject<TObject> : DrawableManiaHitObject public abstract class DrawableManiaHitObject<TObject> : DrawableManiaHitObject
where TObject : ManiaHitObject where TObject : ManiaHitObject
{ {
public new readonly TObject HitObject; public new TObject HitObject => (TObject)base.HitObject;
protected DrawableManiaHitObject(TObject hitObject) protected DrawableManiaHitObject(TObject hitObject)
: base(hitObject) : base(hitObject)
{ {
HitObject = hitObject;
} }
} }
} }

View File

@ -33,31 +33,37 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected virtual ManiaSkinComponents Component => ManiaSkinComponents.Note; protected virtual ManiaSkinComponents Component => ManiaSkinComponents.Note;
private readonly Drawable headPiece; private Drawable headPiece;
public DrawableNote()
: this(null)
{
}
public DrawableNote(Note hitObject) public DrawableNote(Note hitObject)
: base(hitObject) : base(hitObject)
{ {
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
AddInternal(headPiece = new SkinnableDrawable(new ManiaSkinComponent(Component, hitObject.Column), _ => new DefaultNotePiece())
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
});
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(ManiaRulesetConfigManager rulesetConfig) private void load(ManiaRulesetConfigManager rulesetConfig)
{ {
rulesetConfig?.BindWith(ManiaRulesetSetting.TimingBasedNoteColouring, configTimingBasedNoteColouring); rulesetConfig?.BindWith(ManiaRulesetSetting.TimingBasedNoteColouring, configTimingBasedNoteColouring);
AddInternal(headPiece = new SkinnableDrawable(new ManiaSkinComponent(Component), _ => new DefaultNotePiece())
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
});
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
HitObject.StartTimeBindable.BindValueChanged(_ => updateSnapColour()); base.LoadComplete();
configTimingBasedNoteColouring.BindValueChanged(_ => updateSnapColour(), true);
configTimingBasedNoteColouring.BindValueChanged(_ => updateSnapColour());
StartTimeBindable.BindValueChanged(_ => updateSnapColour(), true);
} }
protected override void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e) protected override void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e)
@ -102,7 +108,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
private void updateSnapColour() private void updateSnapColour()
{ {
if (beatmap == null) return; if (beatmap == null || HitObject == null) return;
int snapDivisor = beatmap.ControlPointInfo.GetClosestBeatDivisor(HitObject.StartTime); int snapDivisor = beatmap.ControlPointInfo.GetClosestBeatDivisor(HitObject.StartTime);

View File

@ -0,0 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Mania.Objects
{
public class HeadNote : Note
{
}
}

View File

@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Mania.Objects
/// <summary> /// <summary>
/// The head note of the hold. /// The head note of the hold.
/// </summary> /// </summary>
public Note Head { get; private set; } public HeadNote Head { get; private set; }
/// <summary> /// <summary>
/// The tail note of the hold. /// The tail note of the hold.
@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Mania.Objects
createTicks(cancellationToken); createTicks(cancellationToken);
AddNested(Head = new Note AddNested(Head = new HeadNote
{ {
StartTime = StartTime, StartTime = StartTime,
Column = Column, Column = Column,

View File

@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
break; break;
} }
return null; return Source.GetDrawableComponent(component);
} }
private Drawable getResult(HitResult result) private Drawable getResult(HitResult result)

View File

@ -17,6 +17,7 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.UI namespace osu.Game.Rulesets.Mania.UI
@ -55,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
Width = COLUMN_WIDTH; Width = COLUMN_WIDTH;
Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, Index), _ => new DefaultColumnBackground()) Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}; };
@ -66,7 +67,7 @@ namespace osu.Game.Rulesets.Mania.UI
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements // For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
background.CreateProxy(), background.CreateProxy(),
HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both }, HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both },
new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, Index), _ => new DefaultKeyArea()) new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },
@ -83,6 +84,19 @@ namespace osu.Game.Rulesets.Mania.UI
hitPolicy = new OrderedHitPolicy(HitObjectContainer); hitPolicy = new OrderedHitPolicy(HitObjectContainer);
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy()); TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
RegisterPool<Note, DrawableNote>(10, 50);
RegisterPool<HoldNote, DrawableHoldNote>(10, 50);
RegisterPool<HeadNote, DrawableHoldNoteHead>(10, 50);
RegisterPool<TailNote, DrawableHoldNoteTail>(10, 50);
RegisterPool<HoldNoteTick, DrawableHoldNoteTick>(50, 250);
}
protected override void LoadComplete()
{
base.LoadComplete();
NewResult += OnNewResult;
} }
public ColumnType ColumnType { get; set; } public ColumnType ColumnType { get; set; }
@ -98,28 +112,14 @@ namespace osu.Game.Rulesets.Mania.UI
return dependencies; return dependencies;
} }
/// <summary> protected override void OnNewDrawableHitObject(DrawableHitObject drawableHitObject)
/// Adds a DrawableHitObject to this Playfield.
/// </summary>
/// <param name="hitObject">The DrawableHitObject to add.</param>
public override void Add(DrawableHitObject hitObject)
{ {
hitObject.AccentColour.Value = AccentColour; base.OnNewDrawableHitObject(drawableHitObject);
hitObject.OnNewResult += OnNewResult;
DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)hitObject; DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)drawableHitObject;
maniaObject.AccentColour.Value = AccentColour;
maniaObject.CheckHittable = hitPolicy.IsHittable; maniaObject.CheckHittable = hitPolicy.IsHittable;
base.Add(hitObject);
}
public override bool Remove(DrawableHitObject h)
{
if (!base.Remove(h))
return false;
h.OnNewResult -= OnNewResult;
return true;
} }
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Depth = 2, Depth = 2,
}, },
hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget, columnIndex), _ => new DefaultHitTarget()) hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget())
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Depth = 1 Depth = 1

View File

@ -18,7 +18,6 @@ using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -134,20 +133,7 @@ namespace osu.Game.Rulesets.Mania.UI
protected override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant); protected override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant);
public override DrawableHitObject<ManiaHitObject> CreateDrawableRepresentation(ManiaHitObject h) public override DrawableHitObject<ManiaHitObject> CreateDrawableRepresentation(ManiaHitObject h) => null;
{
switch (h)
{
case HoldNote holdNote:
return new DrawableHoldNote(holdNote);
case Note note:
return new DrawableNote(note);
default:
return null;
}
}
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay); protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay);

View File

@ -9,6 +9,7 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osuTK; using osuTK;
@ -56,6 +57,10 @@ namespace osu.Game.Rulesets.Mania.UI
} }
} }
public override void Add(HitObject hitObject) => getStageByColumn(((ManiaHitObject)hitObject).Column).Add(hitObject);
public override bool Remove(HitObject hitObject) => getStageByColumn(((ManiaHitObject)hitObject).Column).Remove(hitObject);
public override void Add(DrawableHitObject h) => getStageByColumn(((ManiaHitObject)h.HitObject).Column).Add(h); public override void Add(DrawableHitObject h) => getStageByColumn(((ManiaHitObject)h.HitObject).Column).Add(h);
public override bool Remove(DrawableHitObject h) => getStageByColumn(((ManiaHitObject)h.HitObject).Column).Remove(h); public override bool Remove(DrawableHitObject h) => getStageByColumn(((ManiaHitObject)h.HitObject).Column).Remove(h);

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.UI
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
InternalChild = skinnableExplosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, column.Index), _ => new DefaultHitExplosion()) InternalChild = skinnableExplosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion), _ => new DefaultHitExplosion())
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}; };

View File

@ -11,6 +11,7 @@ using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
@ -132,33 +133,19 @@ namespace osu.Game.Rulesets.Mania.UI
} }
} }
public override void Add(DrawableHitObject h) protected override void LoadComplete()
{ {
var maniaObject = (ManiaHitObject)h.HitObject; base.LoadComplete();
NewResult += OnNewResult;
int columnIndex = -1;
maniaObject.ColumnBindable.BindValueChanged(_ =>
{
if (columnIndex != -1)
Columns.ElementAt(columnIndex).Remove(h);
columnIndex = maniaObject.Column - firstColumnIndex;
Columns.ElementAt(columnIndex).Add(h);
}, true);
h.OnNewResult += OnNewResult;
} }
public override bool Remove(DrawableHitObject h) public override void Add(HitObject hitObject) => Columns.ElementAt(((ManiaHitObject)hitObject).Column - firstColumnIndex).Add(hitObject);
{
var maniaObject = (ManiaHitObject)h.HitObject;
int columnIndex = maniaObject.Column - firstColumnIndex;
Columns.ElementAt(columnIndex).Remove(h);
h.OnNewResult -= OnNewResult; public override bool Remove(HitObject hitObject) => Columns.ElementAt(((ManiaHitObject)hitObject).Column - firstColumnIndex).Remove(hitObject);
return true;
} public override void Add(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Add(h);
public override bool Remove(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Remove(h);
public void Add(BarLine barline) => base.Add(new DrawableBarLine(barline)); public void Add(BarLine barline) => base.Add(new DrawableBarLine(barline));

View File

@ -5,12 +5,14 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
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.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
@ -32,6 +34,16 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep($"{term} Small", () => SetContents(() => testSingle(7, autoplay))); AddStep($"{term} Small", () => SetContents(() => testSingle(7, autoplay)));
} }
[Test]
public void TestSpinningSamplePitchShift()
{
AddStep("Add spinner", () => SetContents(() => testSingle(5, true, 4000)));
AddUntilStep("Pitch starts low", () => getSpinningSample().Frequency.Value < 0.8);
AddUntilStep("Pitch increases", () => getSpinningSample().Frequency.Value > 0.8);
PausableSkinnableSound getSpinningSample() => drawableSpinner.ChildrenOfType<PausableSkinnableSound>().FirstOrDefault(s => s.Samples.Any(i => i.LookupNames.Any(l => l.Contains("spinnerspin"))));
}
[TestCase(false)] [TestCase(false)]
[TestCase(true)] [TestCase(true)]
public void TestLongSpinner(bool autoplay) public void TestLongSpinner(bool autoplay)
@ -93,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
base.Update(); base.Update();
if (auto) if (auto)
RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * 3)); RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * 2));
} }
} }
} }

View File

@ -243,7 +243,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
int totalCount = Pieces.Count(p => p.IsSelected.Value); int totalCount = Pieces.Count(p => p.IsSelected.Value);
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type.Value == type); int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type.Value == type);
var item = new PathTypeMenuItem(type, () => var item = new TernaryStateRadioMenuItem(type == null ? "Inherit" : type.ToString().Humanize(), MenuItemType.Standard, _ =>
{ {
foreach (var p in Pieces.Where(p => p.IsSelected.Value)) foreach (var p in Pieces.Where(p => p.IsSelected.Value))
updatePathType(p, type); updatePathType(p, type);
@ -258,15 +258,5 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
return item; return item;
} }
private class PathTypeMenuItem : TernaryStateMenuItem
{
public PathTypeMenuItem(PathType? type, Action action)
: base(type == null ? "Inherit" : type.ToString().Humanize(), changeState, MenuItemType.Standard, _ => action?.Invoke())
{
}
private static TernaryState changeState(TernaryState state) => TernaryState.True;
}
} }
} }

View File

@ -16,6 +16,7 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose;
using osuTK; using osuTK;
@ -25,6 +26,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{ {
public class SliderSelectionBlueprint : OsuSelectionBlueprint<Slider> public class SliderSelectionBlueprint : OsuSelectionBlueprint<Slider>
{ {
protected new DrawableSlider DrawableObject => (DrawableSlider)base.DrawableObject;
protected SliderBodyPiece BodyPiece { get; private set; } protected SliderBodyPiece BodyPiece { get; private set; }
protected SliderCircleOverlay HeadOverlay { get; private set; } protected SliderCircleOverlay HeadOverlay { get; private set; }
protected SliderCircleOverlay TailOverlay { get; private set; } protected SliderCircleOverlay TailOverlay { get; private set; }
@ -236,7 +239,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)), new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)),
}; };
public override Vector2 ScreenSpaceSelectionPoint => BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation); // Always refer to the drawable object's slider body so subsequent movement deltas are calculated with updated positions.
public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathOffset)
?? BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true; BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true;

View File

@ -1,7 +1,6 @@
// 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 System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -18,6 +17,17 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
public class OsuSelectionHandler : EditorSelectionHandler public class OsuSelectionHandler : EditorSelectionHandler
{ {
/// <summary>
/// During a transform, the initial origin is stored so it can be used throughout the operation.
/// </summary>
private Vector2? referenceOrigin;
/// <summary>
/// During a transform, the initial path types of a single selected slider are stored so they
/// can be maintained throughout the operation.
/// </summary>
private List<PathType?> referencePathTypes;
protected override void OnSelectionChanged() protected override void OnSelectionChanged()
{ {
base.OnSelectionChanged(); base.OnSelectionChanged();
@ -50,17 +60,6 @@ namespace osu.Game.Rulesets.Osu.Edit
return true; return true;
} }
/// <summary>
/// During a transform, the initial origin is stored so it can be used throughout the operation.
/// </summary>
private Vector2? referenceOrigin;
/// <summary>
/// During a transform, the initial path types of a single selected slider are stored so they
/// can be maintained throughout the operation.
/// </summary>
private List<PathType?> referencePathTypes;
public override bool HandleReverse() public override bool HandleReverse()
{ {
var hitObjects = EditorBeatmap.SelectedHitObjects; var hitObjects = EditorBeatmap.SelectedHitObjects;
@ -114,24 +113,10 @@ namespace osu.Game.Rulesets.Osu.Edit
var hitObjects = selectedMovableObjects; var hitObjects = selectedMovableObjects;
var selectedObjectsQuad = getSurroundingQuad(hitObjects); var selectedObjectsQuad = getSurroundingQuad(hitObjects);
var centre = selectedObjectsQuad.Centre;
foreach (var h in hitObjects) foreach (var h in hitObjects)
{ {
var pos = h.Position; h.Position = GetFlippedPosition(direction, selectedObjectsQuad, h.Position);
switch (direction)
{
case Direction.Horizontal:
pos.X = centre.X - (pos.X - centre.X);
break;
case Direction.Vertical:
pos.Y = centre.Y - (pos.Y - centre.Y);
break;
}
h.Position = pos;
if (h is Slider slider) if (h is Slider slider)
{ {
@ -187,12 +172,12 @@ namespace osu.Game.Rulesets.Osu.Edit
foreach (var h in hitObjects) foreach (var h in hitObjects)
{ {
h.Position = rotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta); h.Position = RotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);
if (h is IHasPath path) if (h is IHasPath path)
{ {
foreach (var point in path.Path.ControlPoints) foreach (var point in path.Path.ControlPoints)
point.Position.Value = rotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta); point.Position.Value = RotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta);
} }
} }
@ -204,7 +189,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type.Value).ToList(); referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type.Value).ToList();
Quad sliderQuad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value)); Quad sliderQuad = GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0. // Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size; scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
@ -239,26 +224,10 @@ namespace osu.Game.Rulesets.Osu.Edit
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale) private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
{ {
scale = getClampedScale(hitObjects, reference, scale); scale = getClampedScale(hitObjects, reference, scale);
// move the selection before scaling if dragging from top or left anchors.
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
Quad selectionQuad = getSurroundingQuad(hitObjects); Quad selectionQuad = getSurroundingQuad(hitObjects);
foreach (var h in hitObjects) foreach (var h in hitObjects)
{ h.Position = GetScaledPosition(reference, scale, selectionQuad, h.Position);
var newPosition = h.Position;
// guard against no-ops and NaN.
if (scale.X != 0 && selectionQuad.Width > 0)
newPosition.X = selectionQuad.TopLeft.X + xOffset + (h.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X);
if (scale.Y != 0 && selectionQuad.Height > 0)
newPosition.Y = selectionQuad.TopLeft.Y + yOffset + (h.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y);
h.Position = newPosition;
}
} }
private (bool X, bool Y) isQuadInBounds(Quad quad) private (bool X, bool Y) isQuadInBounds(Quad quad)
@ -333,7 +302,7 @@ namespace osu.Game.Rulesets.Osu.Edit
/// </summary> /// </summary>
/// <param name="hitObjects">The hit objects to calculate a quad for.</param> /// <param name="hitObjects">The hit objects to calculate a quad for.</param>
private Quad getSurroundingQuad(OsuHitObject[] hitObjects) => private Quad getSurroundingQuad(OsuHitObject[] hitObjects) =>
getSurroundingQuad(hitObjects.SelectMany(h => GetSurroundingQuad(hitObjects.SelectMany(h =>
{ {
if (h is IHasPath path) if (h is IHasPath path)
{ {
@ -348,58 +317,11 @@ namespace osu.Game.Rulesets.Osu.Edit
return new[] { h.Position }; return new[] { h.Position };
})); }));
/// <summary>
/// Returns a gamefield-space quad surrounding the provided points.
/// </summary>
/// <param name="points">The points to calculate a quad for.</param>
private Quad getSurroundingQuad(IEnumerable<Vector2> points)
{
if (!EditorBeatmap.SelectedHitObjects.Any())
return new Quad();
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
foreach (var p in points)
{
minPosition = Vector2.ComponentMin(minPosition, p);
maxPosition = Vector2.ComponentMax(maxPosition, p);
}
Vector2 size = maxPosition - minPosition;
return new Quad(minPosition.X, minPosition.Y, size.X, size.Y);
}
/// <summary> /// <summary>
/// All osu! hitobjects which can be moved/rotated/scaled. /// All osu! hitobjects which can be moved/rotated/scaled.
/// </summary> /// </summary>
private OsuHitObject[] selectedMovableObjects => SelectedItems.OfType<OsuHitObject>() private OsuHitObject[] selectedMovableObjects => SelectedItems.OfType<OsuHitObject>()
.Where(h => !(h is Spinner)) .Where(h => !(h is Spinner))
.ToArray(); .ToArray();
/// <summary>
/// Rotate a point around an arbitrary origin.
/// </summary>
/// <param name="point">The point.</param>
/// <param name="origin">The centre origin to rotate around.</param>
/// <param name="angle">The angle to rotate (in degrees).</param>
private static Vector2 rotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle)
{
angle = -angle;
point.X -= origin.X;
point.Y -= origin.Y;
Vector2 ret;
ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
ret.X += origin.X;
ret.Y += origin.Y;
return ret;
}
} }
} }

View File

@ -34,7 +34,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public override bool DisplayResult => !HitObject.OnlyJudgeNestedObjects; public override bool DisplayResult => !HitObject.OnlyJudgeNestedObjects;
private PlaySliderBody sliderBody => Body.Drawable as PlaySliderBody; [CanBeNull]
public PlaySliderBody SliderBody => Body.Drawable as PlaySliderBody;
public IBindable<int> PathVersion => pathVersion; public IBindable<int> PathVersion => pathVersion;
private readonly Bindable<int> pathVersion = new Bindable<int>(); private readonly Bindable<int> pathVersion = new Bindable<int>();
@ -215,16 +216,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1); double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);
Ball.UpdateProgress(completionProgress); Ball.UpdateProgress(completionProgress);
sliderBody?.UpdateProgress(completionProgress); SliderBody?.UpdateProgress(completionProgress);
foreach (DrawableHitObject hitObject in NestedHitObjects) foreach (DrawableHitObject hitObject in NestedHitObjects)
{ {
if (hitObject is ITrackSnaking s) s.UpdateSnakingPosition(HitObject.Path.PositionAt(sliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(sliderBody?.SnakedEnd ?? 0)); if (hitObject is ITrackSnaking s) s.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0));
if (hitObject is IRequireTracking t) t.Tracking = Ball.Tracking; if (hitObject is IRequireTracking t) t.Tracking = Ball.Tracking;
} }
Size = sliderBody?.Size ?? Vector2.Zero; Size = SliderBody?.Size ?? Vector2.Zero;
OriginPosition = sliderBody?.PathOffset ?? Vector2.Zero; OriginPosition = SliderBody?.PathOffset ?? Vector2.Zero;
if (DrawSize != Vector2.Zero) if (DrawSize != Vector2.Zero)
{ {
@ -238,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public override void OnKilled() public override void OnKilled()
{ {
base.OnKilled(); base.OnKilled();
sliderBody?.RecyclePath(); SliderBody?.RecyclePath();
} }
protected override void ApplySkin(ISkinSource skin, bool allowFallback) protected override void ApplySkin(ISkinSource skin, bool allowFallback)
@ -324,7 +325,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
case ArmedState.Hit: case ArmedState.Hit:
Ball.ScaleTo(HitObject.Scale * 1.4f, fade_out_time, Easing.Out); Ball.ScaleTo(HitObject.Scale * 1.4f, fade_out_time, Easing.Out);
if (sliderBody?.SnakingOut.Value == true) if (SliderBody?.SnakingOut.Value == true)
Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear. Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear.
break; break;
} }
@ -332,7 +333,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
this.FadeOut(fade_out_time, Easing.OutQuint).Expire(); this.FadeOut(fade_out_time, Easing.OutQuint).Expire();
} }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => sliderBody?.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SliderBody?.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos);
private class DefaultSliderBody : PlaySliderBody private class DefaultSliderBody : PlaySliderBody
{ {

View File

@ -7,13 +7,14 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking, IHasMainCirclePiece public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, IHasMainCirclePiece
{ {
public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject; public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject;
@ -111,7 +112,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult); ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
} }
public void UpdateSnakingPosition(Vector2 start, Vector2 end) => protected override void OnApply()
Position = HitObject.RepeatIndex % 2 == 0 ? end : start; {
base.OnApply();
if (Slider != null)
Position = Slider.CurvePositionAt(HitObject.RepeatIndex % 2 == 0 ? 1 : 0);
}
} }
} }

View File

@ -39,6 +39,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private Bindable<bool> isSpinning; private Bindable<bool> isSpinning;
private bool spinnerFrequencyModulate; private bool spinnerFrequencyModulate;
private const float spinning_sample_initial_frequency = 1.0f;
private const float spinning_sample_modulated_base_frequency = 0.5f;
/// <summary> /// <summary>
/// The amount of bonus score gained from spinning after the required number of spins, for display purposes. /// The amount of bonus score gained from spinning after the required number of spins, for display purposes.
/// </summary> /// </summary>
@ -106,9 +109,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
isSpinning.BindValueChanged(updateSpinningSample); isSpinning.BindValueChanged(updateSpinningSample);
} }
private const float spinning_sample_initial_frequency = 1.0f;
private const float spinning_sample_modulated_base_frequency = 0.5f;
protected override void OnFree() protected override void OnFree()
{ {
base.OnFree(); base.OnFree();

View File

@ -34,9 +34,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
public override Drawable GetDrawableComponent(ISkinComponent component) public override Drawable GetDrawableComponent(ISkinComponent component)
{ {
if (!(component is OsuSkinComponent osuComponent)) if (component is OsuSkinComponent osuComponent)
return null; {
switch (osuComponent.Component) switch (osuComponent.Component)
{ {
case OsuSkinComponents.FollowPoint: case OsuSkinComponents.FollowPoint:
@ -116,8 +115,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null; return null;
} }
}
return null; return Source.GetDrawableComponent(component);
} }
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)

View File

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
[SetUpSteps] [SetUpSteps]
public void SetUp() public void SetUp()
=> AddStep("clear SHOC", () => hitObjectContainer.Clear(false)); => AddStep("clear SHOC", () => hitObjectContainer.Clear());
protected void AddHitObject(DrawableHitObject hitObject) protected void AddHitObject(DrawableHitObject hitObject)
=> AddStep("add to SHOC", () => hitObjectContainer.Add(hitObject)); => AddStep("add to SHOC", () => hitObjectContainer.Add(hitObject));

View File

@ -79,8 +79,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
// Old osu! used hit sounding to determine various hit type information // Old osu! used hit sounding to determine various hit type information
IList<HitSampleInfo> samples = obj.Samples; IList<HitSampleInfo> samples = obj.Samples;
bool strong = samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH);
switch (obj) switch (obj)
{ {
case IHasDistance distanceData: case IHasDistance distanceData:
@ -94,15 +92,11 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
for (double j = obj.StartTime; j <= obj.StartTime + taikoDuration + tickSpacing / 8; j += tickSpacing) for (double j = obj.StartTime; j <= obj.StartTime + taikoDuration + tickSpacing / 8; j += tickSpacing)
{ {
IList<HitSampleInfo> currentSamples = allSamples[i]; IList<HitSampleInfo> currentSamples = allSamples[i];
bool isRim = currentSamples.Any(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE);
strong = currentSamples.Any(s => s.Name == HitSampleInfo.HIT_FINISH);
yield return new Hit yield return new Hit
{ {
StartTime = j, StartTime = j,
Type = isRim ? HitType.Rim : HitType.Centre,
Samples = currentSamples, Samples = currentSamples,
IsStrong = strong
}; };
i = (i + 1) % allSamples.Count; i = (i + 1) % allSamples.Count;
@ -117,7 +111,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{ {
StartTime = obj.StartTime, StartTime = obj.StartTime,
Samples = obj.Samples, Samples = obj.Samples,
IsStrong = strong,
Duration = taikoDuration, Duration = taikoDuration,
TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4 TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4
}; };
@ -143,16 +136,10 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
default: default:
{ {
bool isRimDefinition(HitSampleInfo s) => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE;
bool isRim = samples.Any(isRimDefinition);
yield return new Hit yield return new Hit
{ {
StartTime = obj.StartTime, StartTime = obj.StartTime,
Type = isRim ? HitType.Rim : HitType.Centre,
Samples = samples, Samples = samples,
IsStrong = strong
}; };
break; break;

View File

@ -14,10 +14,10 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
{ {
private readonly HitPiece piece; private readonly HitPiece piece;
private static Hit hit; public new Hit HitObject => (Hit)base.HitObject;
public HitPlacementBlueprint() public HitPlacementBlueprint()
: base(hit = new Hit()) : base(new Hit())
{ {
InternalChild = piece = new HitPiece InternalChild = piece = new HitPiece
{ {
@ -30,12 +30,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
switch (e.Button) switch (e.Button)
{ {
case MouseButton.Left: case MouseButton.Left:
hit.Type = HitType.Centre; HitObject.Type = HitType.Centre;
EndPlacement(true); EndPlacement(true);
return true; return true;
case MouseButton.Right: case MouseButton.Right:
hit.Type = HitType.Rim; HitObject.Type = HitType.Rim;
EndPlacement(true); EndPlacement(true);
return true; return true;
} }

View File

@ -69,17 +69,21 @@ namespace osu.Game.Rulesets.Taiko.Edit
{ {
EditorBeatmap.PerformOnSelection(h => EditorBeatmap.PerformOnSelection(h =>
{ {
if (h is Hit taikoHit) taikoHit.Type = state ? HitType.Rim : HitType.Centre; if (h is Hit taikoHit)
{
taikoHit.Type = state ? HitType.Rim : HitType.Centre;
EditorBeatmap.Update(h);
}
}); });
} }
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection) protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
{ {
if (selection.All(s => s.Item is Hit)) if (selection.All(s => s.Item is Hit))
yield return new TernaryStateMenuItem("Rim") { State = { BindTarget = selectionRimState } }; yield return new TernaryStateToggleMenuItem("Rim") { State = { BindTarget = selectionRimState } };
if (selection.All(s => s.Item is TaikoHitObject)) if (selection.All(s => s.Item is TaikoHitObject))
yield return new TernaryStateMenuItem("Strong") { State = { BindTarget = selectionStrongState } }; yield return new TernaryStateToggleMenuItem("Strong") { State = { BindTarget = selectionStrongState } };
foreach (var item in base.GetContextMenuItemsForSelection(selection)) foreach (var item in base.GetContextMenuItemsForSelection(selection))
yield return item; yield return item;

View File

@ -52,23 +52,18 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override void OnApply() protected override void OnApply()
{ {
type.BindTo(HitObject.TypeBindable); type.BindTo(HitObject.TypeBindable);
type.BindValueChanged(_ => // this doesn't need to be run inline as RecreatePieces is called by the base call below.
{ type.BindValueChanged(_ => Scheduler.AddOnce(RecreatePieces));
updateActionsFromType();
// will overwrite samples, should only be called on subsequent changes
// after the initial application.
updateSamplesFromTypeChange();
RecreatePieces();
});
// action update also has to happen immediately on application.
updateActionsFromType();
base.OnApply(); base.OnApply();
} }
protected override void RecreatePieces()
{
updateActionsFromType();
base.RecreatePieces();
}
protected override void OnFree() protected override void OnFree()
{ {
base.OnFree(); base.OnFree();
@ -83,33 +78,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
validActionPressed = pressHandledThisFrame = false; validActionPressed = pressHandledThisFrame = false;
} }
private HitSampleInfo[] getRimSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray();
protected override void LoadSamples()
{
base.LoadSamples();
type.Value = getRimSamples().Any() ? HitType.Rim : HitType.Centre;
}
private void updateSamplesFromTypeChange()
{
var rimSamples = getRimSamples();
bool isRimType = HitObject.Type == HitType.Rim;
if (isRimType != rimSamples.Any())
{
if (isRimType)
HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
else
{
foreach (var sample in rimSamples)
HitObject.Samples.Remove(sample);
}
}
}
private void updateActionsFromType() private void updateActionsFromType()
{ {
HitActions = HitActions =

View File

@ -137,7 +137,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{ {
Size = BaseSize = new Vector2(TaikoHitObject.DEFAULT_SIZE); Size = BaseSize = new Vector2(TaikoHitObject.DEFAULT_SIZE);
MainPiece?.Expire(); if (MainPiece != null)
Content.Remove(MainPiece);
Content.Add(MainPiece = CreateMainPiece()); Content.Add(MainPiece = CreateMainPiece());
} }

View File

@ -1,11 +1,9 @@
// 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.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osuTK; using osuTK;
@ -29,14 +27,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override void OnApply() protected override void OnApply()
{ {
isStrong.BindTo(HitObject.IsStrongBindable); isStrong.BindTo(HitObject.IsStrongBindable);
isStrong.BindValueChanged(_ => // this doesn't need to be run inline as RecreatePieces is called by the base call below.
{ isStrong.BindValueChanged(_ => Scheduler.AddOnce(RecreatePieces));
// will overwrite samples, should only be called on subsequent changes
// after the initial application.
updateSamplesFromStrong();
RecreatePieces();
});
base.OnApply(); base.OnApply();
} }
@ -50,30 +42,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
isStrong.UnbindEvents(); isStrong.UnbindEvents();
} }
private HitSampleInfo[] getStrongSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray();
protected override void LoadSamples()
{
base.LoadSamples();
isStrong.Value = getStrongSamples().Any();
}
private void updateSamplesFromStrong()
{
var strongSamples = getStrongSamples();
if (isStrong.Value != strongSamples.Any())
{
if (isStrong.Value)
HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
else
{
foreach (var sample in strongSamples)
HitObject.Samples.Remove(sample);
}
}
}
protected override void RecreatePieces() protected override void RecreatePieces()
{ {
base.RecreatePieces(); base.RecreatePieces();

View File

@ -1,7 +1,9 @@
// 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.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Audio;
namespace osu.Game.Rulesets.Taiko.Objects namespace osu.Game.Rulesets.Taiko.Objects
{ {
@ -18,6 +20,40 @@ namespace osu.Game.Rulesets.Taiko.Objects
set => TypeBindable.Value = value; set => TypeBindable.Value = value;
} }
public Hit()
{
TypeBindable.BindValueChanged(_ => updateSamplesFromType());
SamplesBindable.BindCollectionChanged((_, __) => updateTypeFromSamples());
}
private void updateTypeFromSamples()
{
Type = getRimSamples().Any() ? HitType.Rim : HitType.Centre;
}
/// <summary>
/// Returns an array of any samples which would cause this object to be a "rim" type hit.
/// </summary>
private HitSampleInfo[] getRimSamples() => Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray();
private void updateSamplesFromType()
{
var rimSamples = getRimSamples();
bool isRimType = Type == HitType.Rim;
if (isRimType != rimSamples.Any())
{
if (isRimType)
Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
else
{
foreach (var sample in rimSamples)
Samples.Remove(sample);
}
}
}
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime }; protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
public class StrongNestedHit : StrongNestedHitObject public class StrongNestedHit : StrongNestedHitObject

View File

@ -1,8 +1,10 @@
// 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.Linq;
using System.Threading; using System.Threading;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Taiko.Objects namespace osu.Game.Rulesets.Taiko.Objects
@ -34,6 +36,35 @@ namespace osu.Game.Rulesets.Taiko.Objects
set => IsStrongBindable.Value = value; set => IsStrongBindable.Value = value;
} }
protected TaikoStrongableHitObject()
{
IsStrongBindable.BindValueChanged(_ => updateSamplesFromType());
SamplesBindable.BindCollectionChanged((_, __) => updateTypeFromSamples());
}
private void updateTypeFromSamples()
{
IsStrong = getStrongSamples().Any();
}
private void updateSamplesFromType()
{
var strongSamples = getStrongSamples();
if (IsStrongBindable.Value != strongSamples.Any())
{
if (IsStrongBindable.Value)
Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
else
{
foreach (var sample in strongSamples)
Samples.Remove(sample);
}
}
}
private HitSampleInfo[] getStrongSamples() => Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray();
protected override void CreateNestedHitObjects(CancellationToken cancellationToken) protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{ {
base.CreateNestedHitObjects(cancellationToken); base.CreateNestedHitObjects(cancellationToken);

View File

@ -38,9 +38,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
return Drawable.Empty().With(d => d.Expire()); return Drawable.Empty().With(d => d.Expire());
} }
if (!(component is TaikoSkinComponent taikoComponent)) if (component is TaikoSkinComponent taikoComponent)
return null; {
switch (taikoComponent.Component) switch (taikoComponent.Component)
{ {
case TaikoSkinComponents.DrumRollBody: case TaikoSkinComponents.DrumRollBody:
@ -131,6 +130,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
case TaikoSkinComponents.Mascot: case TaikoSkinComponents.Mascot:
return new DrawableTaikoMascot(); return new DrawableTaikoMascot();
} }
}
return Source.GetDrawableComponent(component); return Source.GetDrawableComponent(component);
} }

View File

@ -169,6 +169,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
protected override Track GetBeatmapTrack() => throw new NotImplementedException(); protected override Track GetBeatmapTrack() => throw new NotImplementedException();
protected override ISkin GetSkin() => throw new NotImplementedException();
public override Stream GetStream(string storagePath) => throw new NotImplementedException(); public override Stream GetStream(string storagePath) => throw new NotImplementedException();
} }
} }

View File

@ -23,7 +23,7 @@ namespace osu.Game.Tests.Collections.IO
{ {
var osu = LoadOsuIntoHost(host); var osu = LoadOsuIntoHost(host);
await osu.CollectionManager.Import(new MemoryStream()); await importCollectionsFromStream(osu, new MemoryStream());
Assert.That(osu.CollectionManager.Collections.Count, Is.Zero); Assert.That(osu.CollectionManager.Collections.Count, Is.Zero);
} }
@ -43,7 +43,7 @@ namespace osu.Game.Tests.Collections.IO
{ {
var osu = LoadOsuIntoHost(host); var osu = LoadOsuIntoHost(host);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2)); Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
@ -69,7 +69,7 @@ namespace osu.Game.Tests.Collections.IO
{ {
var osu = LoadOsuIntoHost(host, true); var osu = LoadOsuIntoHost(host, true);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2)); Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
@ -110,7 +110,7 @@ namespace osu.Game.Tests.Collections.IO
ms.Seek(0, SeekOrigin.Begin); ms.Seek(0, SeekOrigin.Begin);
await osu.CollectionManager.Import(ms); await importCollectionsFromStream(osu, ms);
} }
Assert.That(host.UpdateThread.Running, Is.True); Assert.That(host.UpdateThread.Running, Is.True);
@ -134,7 +134,7 @@ namespace osu.Game.Tests.Collections.IO
{ {
var osu = LoadOsuIntoHost(host, true); var osu = LoadOsuIntoHost(host, true);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
// Move first beatmap from second collection into the first. // Move first beatmap from second collection into the first.
osu.CollectionManager.Collections[0].Beatmaps.Add(osu.CollectionManager.Collections[1].Beatmaps[0]); osu.CollectionManager.Collections[0].Beatmaps.Add(osu.CollectionManager.Collections[1].Beatmaps[0]);
@ -169,5 +169,12 @@ namespace osu.Game.Tests.Collections.IO
} }
} }
} }
private static async Task importCollectionsFromStream(TestOsuGameBase osu, Stream stream)
{
// intentionally spin this up on a separate task to avoid disposal deadlocks.
// see https://github.com/EventStore/EventStore/issues/1179
await Task.Run(() => osu.CollectionManager.Import(stream).Wait());
}
} }
} }

View File

@ -0,0 +1,175 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Compose;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Editing
{
[HeadlessTest]
public class TestSceneHitObjectContainerEventBuffer : OsuTestScene
{
private readonly TestHitObject testObj = new TestHitObject();
private TestPlayfield playfield1;
private TestPlayfield playfield2;
private TestDrawable intermediateDrawable;
private HitObjectUsageEventBuffer eventBuffer;
private HitObject beganUsage;
private HitObject finishedUsage;
private HitObject transferredUsage;
[SetUp]
public void Setup() => Schedule(() =>
{
reset();
if (eventBuffer != null)
{
eventBuffer.HitObjectUsageBegan -= onHitObjectUsageBegan;
eventBuffer.HitObjectUsageFinished -= onHitObjectUsageFinished;
eventBuffer.HitObjectUsageTransferred -= onHitObjectUsageTransferred;
}
var topPlayfield = new TestPlayfield();
topPlayfield.AddNested(playfield1 = new TestPlayfield());
topPlayfield.AddNested(playfield2 = new TestPlayfield());
eventBuffer = new HitObjectUsageEventBuffer(topPlayfield);
eventBuffer.HitObjectUsageBegan += onHitObjectUsageBegan;
eventBuffer.HitObjectUsageFinished += onHitObjectUsageFinished;
eventBuffer.HitObjectUsageTransferred += onHitObjectUsageTransferred;
Children = new Drawable[]
{
topPlayfield,
intermediateDrawable = new TestDrawable(),
};
});
private void onHitObjectUsageBegan(HitObject obj) => beganUsage = obj;
private void onHitObjectUsageFinished(HitObject obj) => finishedUsage = obj;
private void onHitObjectUsageTransferred(HitObject obj, DrawableHitObject drawableObj) => transferredUsage = obj;
[Test]
public void TestUsageBeganAfterAdd()
{
AddStep("add hitobject", () => playfield1.Add(testObj));
addCheckStep(began: true);
}
[Test]
public void TestUsageFinishedAfterRemove()
{
AddStep("add hitobject", () => playfield1.Add(testObj));
addResetStep();
AddStep("remove hitobject", () => playfield1.Remove(testObj));
addCheckStep(finished: true);
}
[Test]
public void TestUsageTransferredWhenMovedBetweenPlayfields()
{
AddStep("add hitobject", () => playfield1.Add(testObj));
addResetStep();
AddStep("transfer hitobject to other playfield", () =>
{
playfield1.Remove(testObj);
playfield2.Add(testObj);
});
addCheckStep(transferred: true);
}
[Test]
public void TestRemoveImmediatelyAfterUsageBegan()
{
AddStep("add hitobject and schedule removal", () =>
{
playfield1.Add(testObj);
intermediateDrawable.Schedule(() => playfield1.Remove(testObj));
});
addCheckStep(began: true, finished: true);
}
[Test]
public void TestRemoveImmediatelyAfterTransferred()
{
AddStep("add hitobject", () => playfield1.Add(testObj));
addResetStep();
AddStep("transfer hitobject to other playfield and schedule removal", () =>
{
playfield1.Remove(testObj);
playfield2.Add(testObj);
intermediateDrawable.Schedule(() => playfield2.Remove(testObj));
});
addCheckStep(transferred: true, finished: true);
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
eventBuffer.Update();
}
private void addResetStep() => AddStep("reset", reset);
private void reset()
{
beganUsage = null;
finishedUsage = null;
transferredUsage = null;
}
private void addCheckStep(bool began = false, bool finished = false, bool transferred = false)
=> AddAssert($"began = {began}, finished = {finished}, transferred = {transferred}",
() => (beganUsage == testObj) == began && (finishedUsage == testObj) == finished && (transferredUsage == testObj) == transferred);
private class TestPlayfield : Playfield
{
public TestPlayfield()
{
RegisterPool<TestHitObject, TestDrawableHitObject>(1);
}
public new void AddNested(Playfield playfield)
{
AddInternal(playfield);
base.AddNested(playfield);
}
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject)
{
var entry = base.CreateLifetimeEntry(hitObject);
entry.KeepAlive = true;
return entry;
}
}
private class TestHitObject : HitObject
{
public override string ToString() => "TestHitObject";
}
private class TestDrawableHitObject : DrawableHitObject
{
}
private class TestDrawable : Drawable
{
public new void Schedule(Action action) => base.Schedule(action);
}
}
}

View File

@ -3,6 +3,8 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -45,15 +47,16 @@ namespace osu.Game.Tests.Gameplay
AddStep("Create DHO", () => AddStep("Create DHO", () =>
{ {
dho = new TestDrawableHitObject(null); dho = new TestDrawableHitObject(null);
dho.Apply(entry = new TestLifetimeEntry(new HitObject()) dho.Apply(entry = new TestLifetimeEntry(new HitObject()));
{
LifetimeStart = 0,
LifetimeEnd = 1000,
});
Child = dho; Child = dho;
}); });
AddStep("KeepAlive = true", () => entry.KeepAlive = true); AddStep("KeepAlive = true", () =>
{
entry.LifetimeStart = 0;
entry.LifetimeEnd = 1000;
entry.KeepAlive = true;
});
AddAssert("Lifetime is overriden", () => entry.LifetimeStart == double.MinValue && entry.LifetimeEnd == double.MaxValue); AddAssert("Lifetime is overriden", () => entry.LifetimeStart == double.MinValue && entry.LifetimeEnd == double.MaxValue);
AddStep("Set LifetimeStart", () => dho.LifetimeStart = 500); AddStep("Set LifetimeStart", () => dho.LifetimeStart = 500);
@ -69,15 +72,46 @@ namespace osu.Game.Tests.Gameplay
AddAssert("Lifetime is changed", () => entry.LifetimeStart == double.MinValue && entry.LifetimeEnd == 1000); AddAssert("Lifetime is changed", () => entry.LifetimeStart == double.MinValue && entry.LifetimeEnd == 1000);
} }
[Test]
public void TestLifetimeUpdatedOnDefaultApplied()
{
TestLifetimeEntry entry = null;
AddStep("Create entry", () => entry = new TestLifetimeEntry(new HitObject()) { LifetimeStart = 1 });
AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()));
AddAssert("Lifetime is updated", () => entry.LifetimeStart == -TestLifetimeEntry.INITIAL_LIFETIME_OFFSET);
TestDrawableHitObject dho = null;
AddStep("Create DHO", () =>
{
dho = new TestDrawableHitObject(null);
dho.Apply(entry);
Child = dho;
dho.SetLifetimeStartOnApply = true;
});
AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()));
AddAssert("Lifetime is correct", () => dho.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY && entry.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY);
}
private class TestDrawableHitObject : DrawableHitObject private class TestDrawableHitObject : DrawableHitObject
{ {
public const double INITIAL_LIFETIME_OFFSET = 100; public const double INITIAL_LIFETIME_OFFSET = 100;
public const double LIFETIME_ON_APPLY = 222;
protected override double InitialLifetimeOffset => INITIAL_LIFETIME_OFFSET; protected override double InitialLifetimeOffset => INITIAL_LIFETIME_OFFSET;
public bool SetLifetimeStartOnApply;
public TestDrawableHitObject(HitObject hitObject) public TestDrawableHitObject(HitObject hitObject)
: base(hitObject) : base(hitObject)
{ {
} }
protected override void OnApply()
{
base.OnApply();
if (SetLifetimeStartOnApply)
LifetimeStart = LIFETIME_ON_APPLY;
}
} }
private class TestLifetimeEntry : HitObjectLifetimeEntry private class TestLifetimeEntry : HitObjectLifetimeEntry

View File

@ -0,0 +1,85 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Gameplay
{
[HeadlessTest]
public class TestSceneProxyContainer : OsuTestScene
{
private HitObjectContainer hitObjectContainer;
private ProxyContainer proxyContainer;
private readonly ManualClock clock = new ManualClock();
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = new Container
{
Children = new Drawable[]
{
hitObjectContainer = new HitObjectContainer(),
proxyContainer = new ProxyContainer()
},
Clock = new FramedClock(clock)
};
clock.CurrentTime = 0;
});
[Test]
public void TestProxyLifetimeManagement()
{
AddStep("Add proxy drawables", () =>
{
addProxy(new TestDrawableHitObject(1000));
addProxy(new TestDrawableHitObject(3000));
addProxy(new TestDrawableHitObject(5000));
});
AddStep("time = 1000", () => clock.CurrentTime = 1000);
AddAssert("One proxy is alive", () => proxyContainer.AliveChildren.Count == 1);
AddStep("time = 5000", () => clock.CurrentTime = 5000);
AddAssert("One proxy is alive", () => proxyContainer.AliveChildren.Count == 1);
AddStep("time = 6000", () => clock.CurrentTime = 6000);
AddAssert("No proxy is alive", () => proxyContainer.AliveChildren.Count == 0);
}
private void addProxy(DrawableHitObject drawableHitObject)
{
hitObjectContainer.Add(drawableHitObject);
proxyContainer.AddProxy(drawableHitObject);
}
private class ProxyContainer : LifetimeManagementContainer
{
public IReadOnlyList<Drawable> AliveChildren => AliveInternalChildren;
public void AddProxy(Drawable d) => AddInternal(d.CreateProxy());
}
private class TestDrawableHitObject : DrawableHitObject
{
protected override double InitialLifetimeOffset => 100;
public TestDrawableHitObject(double startTime)
: base(new HitObject { StartTime = startTime })
{
}
protected override void UpdateInitialTransforms()
{
LifetimeEnd = LifetimeStart + 500;
}
}
}
}

View File

@ -0,0 +1,130 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Lists;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osu.Game.Storyboards;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneBeatmapSkinFallbacks : OsuPlayerTestScene
{
private ISkin currentBeatmapSkin;
[Resolved]
private SkinManager skinManager { get; set; }
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor();
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
protected override bool HasCustomSteps => true;
[Test]
public void TestEmptyLegacyBeatmapSkinFallsBack()
{
CreateSkinTest(SkinInfo.Default, () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null));
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value));
}
protected void CreateSkinTest(SkinInfo gameCurrentSkin, Func<ISkin> getBeatmapSkin)
{
CreateTest(() =>
{
AddStep("setup skins", () =>
{
skinManager.CurrentSkinInfo.Value = gameCurrentSkin;
currentBeatmapSkin = getBeatmapSkin();
});
});
}
protected bool AssertComponentsFromExpectedSource(SkinnableTarget target, ISkin expectedSource)
{
var actualComponentsContainer = Player.ChildrenOfType<SkinnableTargetContainer>().First(s => s.Target == target)
.ChildrenOfType<SkinnableTargetComponentsContainer>().SingleOrDefault();
if (actualComponentsContainer == null)
return false;
var actualInfo = actualComponentsContainer.CreateSkinnableInfo();
var expectedComponentsContainer = (SkinnableTargetComponentsContainer)expectedSource.GetDrawableComponent(new SkinnableTargetComponent(target));
if (expectedComponentsContainer == null)
return false;
var expectedComponentsAdjustmentContainer = new Container
{
Position = actualComponentsContainer.Parent.ToSpaceOfOtherDrawable(actualComponentsContainer.DrawPosition, Content),
Size = actualComponentsContainer.DrawSize,
Child = expectedComponentsContainer,
};
Add(expectedComponentsAdjustmentContainer);
expectedComponentsAdjustmentContainer.UpdateSubTree();
var expectedInfo = expectedComponentsContainer.CreateSkinnableInfo();
Remove(expectedComponentsAdjustmentContainer);
return almostEqual(actualInfo, expectedInfo);
static bool almostEqual(SkinnableInfo info, SkinnableInfo other) =>
other != null
&& info.Type == other.Type
&& info.Anchor == other.Anchor
&& info.Origin == other.Origin
&& Precision.AlmostEquals(info.Position, other.Position)
&& Precision.AlmostEquals(info.Scale, other.Scale)
&& Precision.AlmostEquals(info.Rotation, other.Rotation)
&& info.Children.SequenceEqual(other.Children, new FuncEqualityComparer<SkinnableInfo>(almostEqual));
}
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
=> new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, Audio, currentBeatmapSkin);
protected override Ruleset CreatePlayerRuleset() => new TestOsuRuleset();
private class CustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap
{
private readonly ISkin beatmapSkin;
public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio, ISkin beatmapSkin)
: base(beatmap, storyboard, referenceClock, audio)
{
this.beatmapSkin = beatmapSkin;
}
protected override ISkin GetSkin() => beatmapSkin;
}
private class TestOsuRuleset : OsuRuleset
{
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new TestOsuLegacySkinTransformer(source);
private class TestOsuLegacySkinTransformer : OsuLegacySkinTransformer
{
public TestOsuLegacySkinTransformer(ISkinSource source)
: base(source)
{
}
}
}
}
}

View File

@ -1,6 +1,9 @@
// 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 System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -11,29 +14,35 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Catch.Scoring; using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Rulesets.Taiko.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Screens.Play.HUD.HitErrorMeters;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestSceneHitErrorMeter : OsuTestScene public class TestSceneHitErrorMeter : OsuTestScene
{ {
private HitWindows hitWindows;
[Cached] [Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(); private ScoreProcessor scoreProcessor = new ScoreProcessor();
[Cached(typeof(DrawableRuleset))]
private TestDrawableRuleset drawableRuleset = new TestDrawableRuleset();
public TestSceneHitErrorMeter() public TestSceneHitErrorMeter()
{ {
recreateDisplay(new OsuHitWindows(), 5); recreateDisplay(new OsuHitWindows(), 5);
AddRepeatStep("New random judgement", () => newJudgement(), 40); AddRepeatStep("New random judgement", () => newJudgement(), 40);
AddRepeatStep("New max negative", () => newJudgement(-hitWindows.WindowFor(HitResult.Meh)), 20); AddRepeatStep("New max negative", () => newJudgement(-drawableRuleset.HitWindows.WindowFor(HitResult.Meh)), 20);
AddRepeatStep("New max positive", () => newJudgement(hitWindows.WindowFor(HitResult.Meh)), 20); AddRepeatStep("New max positive", () => newJudgement(drawableRuleset.HitWindows.WindowFor(HitResult.Meh)), 20);
AddStep("New fixed judgement (50ms)", () => newJudgement(50)); AddStep("New fixed judgement (50ms)", () => newJudgement(50));
AddStep("Judgement barrage", () => AddStep("Judgement barrage", () =>
@ -83,10 +92,10 @@ namespace osu.Game.Tests.Visual.Gameplay
private void recreateDisplay(HitWindows hitWindows, float overallDifficulty) private void recreateDisplay(HitWindows hitWindows, float overallDifficulty)
{ {
this.hitWindows = hitWindows;
hitWindows?.SetDifficulty(overallDifficulty); hitWindows?.SetDifficulty(overallDifficulty);
drawableRuleset.HitWindows = hitWindows;
Clear(); Clear();
Add(new FillFlowContainer Add(new FillFlowContainer
@ -103,40 +112,40 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
}); });
Add(new BarHitErrorMeter(hitWindows, true) Add(new BarHitErrorMeter
{ {
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
}); });
Add(new BarHitErrorMeter(hitWindows, false) Add(new BarHitErrorMeter
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
}); });
Add(new BarHitErrorMeter(hitWindows, true) Add(new BarHitErrorMeter
{ {
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Rotation = 270, Rotation = 270,
}); });
Add(new ColourHitErrorMeter(hitWindows) Add(new ColourHitErrorMeter
{ {
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
Margin = new MarginPadding { Right = 50 } Margin = new MarginPadding { Right = 50 }
}); });
Add(new ColourHitErrorMeter(hitWindows) Add(new ColourHitErrorMeter
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = 50 } Margin = new MarginPadding { Left = 50 }
}); });
Add(new ColourHitErrorMeter(hitWindows) Add(new ColourHitErrorMeter
{ {
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
@ -147,11 +156,47 @@ namespace osu.Game.Tests.Visual.Gameplay
private void newJudgement(double offset = 0) private void newJudgement(double offset = 0)
{ {
scoreProcessor.ApplyResult(new JudgementResult(new HitCircle { HitWindows = hitWindows }, new Judgement()) scoreProcessor.ApplyResult(new JudgementResult(new HitCircle { HitWindows = drawableRuleset.HitWindows }, new Judgement())
{ {
TimeOffset = offset == 0 ? RNG.Next(-150, 150) : offset, TimeOffset = offset == 0 ? RNG.Next(-150, 150) : offset,
Type = HitResult.Perfect, Type = HitResult.Perfect,
}); });
} }
[SuppressMessage("ReSharper", "UnassignedGetOnlyAutoProperty")]
private class TestDrawableRuleset : DrawableRuleset
{
public HitWindows HitWindows;
public override IEnumerable<HitObject> Objects => new[] { new HitCircle { HitWindows = HitWindows } };
public override event Action<JudgementResult> NewResult;
public override event Action<JudgementResult> RevertResult;
public override Playfield Playfield { get; }
public override Container Overlays { get; }
public override Container FrameStableComponents { get; }
public override IFrameStableClock FrameStableClock { get; }
public override IReadOnlyList<Mod> Mods { get; }
public override double GameplayStartTime { get; }
public override GameplayCursorContainer Cursor { get; }
public TestDrawableRuleset()
: base(new OsuRuleset())
{
// won't compile without this.
NewResult?.Invoke(null);
RevertResult?.Invoke(null);
}
public override void SetReplayScore(Score replayScore) => throw new NotImplementedException();
public override void SetRecordTarget(Score score) => throw new NotImplementedException();
public override void RequestResume(Action continueResume) => throw new NotImplementedException();
public override void CancelResume() => throw new NotImplementedException();
}
} }
} }

View File

@ -88,13 +88,18 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
beforeLoadAction?.Invoke(); beforeLoadAction?.Invoke();
prepareBeatmap();
LoadScreen(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive)));
}
private void prepareBeatmap()
{
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
Beatmap.Value.BeatmapInfo.EpilepsyWarning = epilepsyWarning; Beatmap.Value.BeatmapInfo.EpilepsyWarning = epilepsyWarning;
foreach (var mod in SelectedMods.Value.OfType<IApplicableToTrack>()) foreach (var mod in SelectedMods.Value.OfType<IApplicableToTrack>())
mod.ApplyToTrack(Beatmap.Value.Track); mod.ApplyToTrack(Beatmap.Value.Track);
LoadScreen(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive)));
} }
[Test] [Test]
@ -178,10 +183,13 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("load slow dummy beatmap", () => AddStep("load slow dummy beatmap", () =>
{ {
LoadScreen(loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false))); prepareBeatmap();
Scheduler.AddDelayed(() => slowPlayer.AllowLoad.Set(), 5000); slowPlayer = new SlowLoadPlayer(false, false);
LoadScreen(loader = new TestPlayerLoader(() => slowPlayer));
}); });
AddStep("schedule slow load", () => Scheduler.AddDelayed(() => slowPlayer.AllowLoad.Set(), 5000));
AddUntilStep("wait for player to be current", () => slowPlayer.IsCurrentScreen()); AddUntilStep("wait for player to be current", () => slowPlayer.IsCurrentScreen());
} }

View File

@ -27,8 +27,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
private readonly User streamingUser = new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Test user" }; private readonly User streamingUser = new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Test user" };
[Cached(typeof(SpectatorStreamingClient))] [Cached(typeof(SpectatorClient))]
private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient(); private TestSpectatorClient testSpectatorClient = new TestSpectatorClient();
[Cached(typeof(UserLookupCache))] [Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache(); private UserLookupCache lookupCache = new TestUserLookupCache();
@ -61,8 +61,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("add streaming client", () => AddStep("add streaming client", () =>
{ {
Remove(testSpectatorStreamingClient); Remove(testSpectatorClient);
Add(testSpectatorStreamingClient); Add(testSpectatorClient);
}); });
finish(); finish();
@ -212,9 +212,9 @@ namespace osu.Game.Tests.Visual.Gameplay
private void waitForPlayer() => AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); private void waitForPlayer() => AddUntilStep("wait for player", () => Stack.CurrentScreen is Player);
private void start(int? beatmapId = null) => AddStep("start play", () => testSpectatorStreamingClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); private void start(int? beatmapId = null) => AddStep("start play", () => testSpectatorClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId));
private void finish(int? beatmapId = null) => AddStep("end play", () => testSpectatorStreamingClient.EndPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); private void finish() => AddStep("end play", () => testSpectatorClient.EndPlay(streamingUser.Id));
private void checkPaused(bool state) => private void checkPaused(bool state) =>
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state); AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state);
@ -223,7 +223,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
AddStep("send frames", () => AddStep("send frames", () =>
{ {
testSpectatorStreamingClient.SendFrames(streamingUser.Id, nextFrame, count); testSpectatorClient.SendFrames(streamingUser.Id, nextFrame, count);
nextFrame += count; nextFrame += count;
}); });
} }

View File

@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
[Resolved] [Resolved]
private SpectatorStreamingClient streamingClient { get; set; } private SpectatorClient spectatorClient { get; set; }
[Cached] [Cached]
private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap()); private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap());
@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
replay = new Replay(); replay = new Replay();
users.BindTo(streamingClient.PlayingUsers); users.BindTo(spectatorClient.PlayingUsers);
users.BindCollectionChanged((obj, args) => users.BindCollectionChanged((obj, args) =>
{ {
switch (args.Action) switch (args.Action)
@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Gameplay
foreach (int user in args.NewItems) foreach (int user in args.NewItems)
{ {
if (user == api.LocalUser.Value.Id) if (user == api.LocalUser.Value.Id)
streamingClient.WatchUser(user); spectatorClient.WatchUser(user);
} }
break; break;
@ -91,14 +91,14 @@ namespace osu.Game.Tests.Visual.Gameplay
foreach (int user in args.OldItems) foreach (int user in args.OldItems)
{ {
if (user == api.LocalUser.Value.Id) if (user == api.LocalUser.Value.Id)
streamingClient.StopWatchingUser(user); spectatorClient.StopWatchingUser(user);
} }
break; break;
} }
}, true); }, true);
streamingClient.OnNewFrames += onNewFrames; spectatorClient.OnNewFrames += onNewFrames;
Add(new GridContainer Add(new GridContainer
{ {
@ -189,7 +189,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
} }
private double latency = SpectatorStreamingClient.TIME_BETWEEN_SENDS; private double latency = SpectatorClient.TIME_BETWEEN_SENDS;
protected override void Update() protected override void Update()
{ {
@ -233,7 +233,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("stop recorder", () => AddStep("stop recorder", () =>
{ {
recorder.Expire(); recorder.Expire();
streamingClient.OnNewFrames -= onNewFrames; spectatorClient.OnNewFrames -= onNewFrames;
}); });
} }

View File

@ -53,7 +53,8 @@ namespace osu.Game.Tests.Visual.Gameplay
CreateTest(null); CreateTest(null);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("skip outro", () => InputManager.Key(osuTK.Input.Key.Space)); AddStep("skip outro", () => InputManager.Key(osuTK.Input.Key.Space));
AddAssert("score shown", () => Player.IsScoreShown); AddUntilStep("wait for score shown", () => Player.IsScoreShown);
AddUntilStep("time less than storyboard duration", () => Player.GameplayClockContainer.GameplayClock.CurrentTime < currentStoryboardDuration);
} }
[Test] [Test]

View File

@ -23,8 +23,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
public class TestSceneMultiSpectatorLeaderboard : MultiplayerTestScene public class TestSceneMultiSpectatorLeaderboard : MultiplayerTestScene
{ {
[Cached(typeof(SpectatorStreamingClient))] [Cached(typeof(SpectatorClient))]
private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient(); private TestSpectatorClient spectatorClient = new TestSpectatorClient();
[Cached(typeof(UserLookupCache))] [Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache(); private UserLookupCache lookupCache = new TestUserLookupCache();
@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
base.Content.AddRange(new Drawable[] base.Content.AddRange(new Drawable[]
{ {
streamingClient, spectatorClient,
lookupCache, lookupCache,
content = new Container { RelativeSizeAxes = Axes.Both } content = new Container { RelativeSizeAxes = Axes.Both }
}); });
@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
foreach (var (userId, clock) in clocks) foreach (var (userId, clock) in clocks)
{ {
streamingClient.EndPlay(userId, 0); spectatorClient.EndPlay(userId);
clock.CurrentTime = 0; clock.CurrentTime = 0;
} }
}); });
@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create leaderboard", () => AddStep("create leaderboard", () =>
{ {
foreach (var (userId, _) in clocks) foreach (var (userId, _) in clocks)
streamingClient.StartPlay(userId, 0); spectatorClient.StartPlay(userId, 0);
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
@ -96,10 +96,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
// For player 2, send frames in sets of 10. // For player 2, send frames in sets of 10.
for (int i = 0; i < 100; i++) for (int i = 0; i < 100; i++)
{ {
streamingClient.SendFrames(PLAYER_1_ID, i, 1); spectatorClient.SendFrames(PLAYER_1_ID, i, 1);
if (i % 10 == 0) if (i % 10 == 0)
streamingClient.SendFrames(PLAYER_2_ID, i, 10); spectatorClient.SendFrames(PLAYER_2_ID, i, 10);
} }
}); });

View File

@ -22,8 +22,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
public class TestSceneMultiSpectatorScreen : MultiplayerTestScene public class TestSceneMultiSpectatorScreen : MultiplayerTestScene
{ {
[Cached(typeof(SpectatorStreamingClient))] [Cached(typeof(SpectatorClient))]
private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient(); private TestSpectatorClient spectatorClient = new TestSpectatorClient();
[Cached(typeof(UserLookupCache))] [Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache(); private UserLookupCache lookupCache = new TestUserLookupCache();
@ -59,14 +59,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("add streaming client", () => AddStep("add streaming client", () =>
{ {
Remove(streamingClient); Remove(spectatorClient);
Add(streamingClient); Add(spectatorClient);
}); });
AddStep("finish previous gameplay", () => AddStep("finish previous gameplay", () =>
{ {
foreach (var id in playingUserIds) foreach (var id in playingUserIds)
streamingClient.EndPlay(id, importedBeatmapId); spectatorClient.EndPlay(id);
playingUserIds.Clear(); playingUserIds.Clear();
}); });
} }
@ -87,11 +87,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
loadSpectateScreen(false); loadSpectateScreen(false);
AddWaitStep("wait a bit", 10); AddWaitStep("wait a bit", 10);
AddStep("load player first_player_id", () => streamingClient.StartPlay(PLAYER_1_ID, importedBeatmapId)); AddStep("load player first_player_id", () => spectatorClient.StartPlay(PLAYER_1_ID, importedBeatmapId));
AddUntilStep("one player added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 1); AddUntilStep("one player added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 1);
AddWaitStep("wait a bit", 10); AddWaitStep("wait a bit", 10);
AddStep("load player second_player_id", () => streamingClient.StartPlay(PLAYER_2_ID, importedBeatmapId)); AddStep("load player second_player_id", () => spectatorClient.StartPlay(PLAYER_2_ID, importedBeatmapId));
AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 2); AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 2);
} }
@ -251,18 +251,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
foreach (int id in userIds) foreach (int id in userIds)
{ {
Client.CurrentMatchPlayingUserIds.Add(id); Client.CurrentMatchPlayingUserIds.Add(id);
streamingClient.StartPlay(id, beatmapId ?? importedBeatmapId); spectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId);
playingUserIds.Add(id); playingUserIds.Add(id);
nextFrame[id] = 0; nextFrame[id] = 0;
} }
}); });
} }
private void finish(int userId, int? beatmapId = null) private void finish(int userId)
{ {
AddStep("end play", () => AddStep("end play", () =>
{ {
streamingClient.EndPlay(userId, beatmapId ?? importedBeatmapId); spectatorClient.EndPlay(userId);
playingUserIds.Remove(userId); playingUserIds.Remove(userId);
nextFrame.Remove(userId); nextFrame.Remove(userId);
}); });
@ -276,7 +276,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
foreach (int id in userIds) foreach (int id in userIds)
{ {
streamingClient.SendFrames(id, nextFrame[id], count); spectatorClient.SendFrames(id, nextFrame[id], count);
nextFrame[id] += count; nextFrame[id] += count;
} }
}); });

View File

@ -195,7 +195,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
{ {
[Cached(typeof(StatefulMultiplayerClient))] [Cached(typeof(MultiplayerClient))]
public readonly TestMultiplayerClient Client; public readonly TestMultiplayerClient Client;
public TestMultiplayer() public TestMultiplayer()

View File

@ -28,8 +28,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
private const int users = 16; private const int users = 16;
[Cached(typeof(SpectatorStreamingClient))] [Cached(typeof(SpectatorClient))]
private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(); private TestMultiplayerSpectatorClient spectatorClient = new TestMultiplayerSpectatorClient();
[Cached(typeof(UserLookupCache))] [Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache(); private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache();
@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
base.Content.Children = new Drawable[] base.Content.Children = new Drawable[]
{ {
streamingClient, spectatorClient,
lookupCache, lookupCache,
Content Content
}; };
@ -71,10 +71,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
for (int i = 0; i < users; i++) for (int i = 0; i < users; i++)
streamingClient.StartPlay(i, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); spectatorClient.StartPlay(i, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
spectatorClient.Schedule(() =>
{
Client.CurrentMatchPlayingUserIds.Clear(); Client.CurrentMatchPlayingUserIds.Clear();
Client.CurrentMatchPlayingUserIds.AddRange(streamingClient.PlayingUsers); Client.CurrentMatchPlayingUserIds.AddRange(spectatorClient.PlayingUsers);
});
Children = new Drawable[] Children = new Drawable[]
{ {
@ -83,7 +86,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
scoreProcessor.ApplyBeatmap(playable); scoreProcessor.ApplyBeatmap(playable);
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, streamingClient.PlayingUsers.ToArray()) LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, spectatorClient.PlayingUsers.ToArray())
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -91,12 +94,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
}); });
AddUntilStep("wait for load", () => leaderboard.IsLoaded); AddUntilStep("wait for load", () => leaderboard.IsLoaded);
AddUntilStep("wait for user population", () => Client.CurrentMatchPlayingUserIds.Count > 0);
} }
[Test] [Test]
public void TestScoreUpdates() public void TestScoreUpdates()
{ {
AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 100); AddRepeatStep("update state", () => spectatorClient.RandomlyUpdateState(), 100);
AddToggleStep("switch compact mode", expanded => leaderboard.Expanded.Value = expanded); AddToggleStep("switch compact mode", expanded => leaderboard.Expanded.Value = expanded);
} }
@ -109,12 +113,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestChangeScoringMode() public void TestChangeScoringMode()
{ {
AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 5); AddRepeatStep("update state", () => spectatorClient.RandomlyUpdateState(), 5);
AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic)); AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic));
AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised)); AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
} }
public class TestMultiplayerStreaming : TestSpectatorStreamingClient public class TestMultiplayerSpectatorClient : TestSpectatorClient
{ {
private readonly Dictionary<int, FrameHeader> lastHeaders = new Dictionary<int, FrameHeader>(); private readonly Dictionary<int, FrameHeader> lastHeaders = new Dictionary<int, FrameHeader>();

View File

@ -11,8 +11,8 @@ using osu.Game.Overlays;
using osu.Game.Screens; using osu.Game.Screens;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Select;
using osuTK.Input; using osuTK.Input;
using static osu.Game.Tests.Visual.Navigation.TestSceneScreenNavigation;
namespace osu.Game.Tests.Visual.Navigation namespace osu.Game.Tests.Visual.Navigation
{ {
@ -37,17 +37,17 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestPerformAtSongSelect() public void TestPerformAtSongSelect()
{ {
PushAndConfirm(() => new PlaySongSelect()); PushAndConfirm(() => new TestPlaySongSelect());
AddStep("perform immediately", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(PlaySongSelect) })); AddStep("perform immediately", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(TestPlaySongSelect) }));
AddAssert("did perform", () => actionPerformed); AddAssert("did perform", () => actionPerformed);
AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen is TestPlaySongSelect);
} }
[Test] [Test]
public void TestPerformAtMenuFromSongSelect() public void TestPerformAtMenuFromSongSelect()
{ {
PushAndConfirm(() => new PlaySongSelect()); PushAndConfirm(() => new TestPlaySongSelect());
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true)); AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true));
AddUntilStep("returned to menu", () => Game.ScreenStack.CurrentScreen is MainMenu); AddUntilStep("returned to menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
@ -57,18 +57,18 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestPerformAtSongSelectFromPlayerLoader() public void TestPerformAtSongSelectFromPlayerLoader()
{ {
PushAndConfirm(() => new PlaySongSelect()); PushAndConfirm(() => new TestPlaySongSelect());
PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer())); PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer()));
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(PlaySongSelect) })); AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(TestPlaySongSelect) }));
AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is TestPlaySongSelect);
AddAssert("did perform", () => actionPerformed); AddAssert("did perform", () => actionPerformed);
} }
[Test] [Test]
public void TestPerformAtMenuFromPlayerLoader() public void TestPerformAtMenuFromPlayerLoader()
{ {
PushAndConfirm(() => new PlaySongSelect()); PushAndConfirm(() => new TestPlaySongSelect());
PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer())); PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer()));
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true)); AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true));

View File

@ -34,9 +34,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestExitSongSelectWithEscape() public void TestExitSongSelectWithEscape()
{ {
TestSongSelect songSelect = null; TestPlaySongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestSongSelect()); PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show()); AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
pushEscape(); pushEscape();
@ -51,9 +51,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestOpenModSelectOverlayUsingAction() public void TestOpenModSelectOverlayUsingAction()
{ {
TestSongSelect songSelect = null; TestPlaySongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestSongSelect()); PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddStep("Show mods overlay", () => InputManager.Key(Key.F1)); AddStep("Show mods overlay", () => InputManager.Key(Key.F1));
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
} }
@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Navigation
{ {
Player player = null; Player player = null;
PushAndConfirm(() => new TestSongSelect()); PushAndConfirm(() => new TestPlaySongSelect());
AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait()); AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait());
@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Navigation
WorkingBeatmap beatmap() => Game.Beatmap.Value; WorkingBeatmap beatmap() => Game.Beatmap.Value;
PushAndConfirm(() => new TestSongSelect()); PushAndConfirm(() => new TestPlaySongSelect());
AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait()); AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait());
@ -113,7 +113,7 @@ namespace osu.Game.Tests.Visual.Navigation
WorkingBeatmap beatmap() => Game.Beatmap.Value; WorkingBeatmap beatmap() => Game.Beatmap.Value;
PushAndConfirm(() => new TestSongSelect()); PushAndConfirm(() => new TestPlaySongSelect());
AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).Wait()); AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).Wait());
@ -139,9 +139,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestMenuMakesMusic() public void TestMenuMakesMusic()
{ {
TestSongSelect songSelect = null; TestPlaySongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestSongSelect()); PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for no track", () => Game.MusicController.CurrentTrack.IsDummyDevice); AddUntilStep("wait for no track", () => Game.MusicController.CurrentTrack.IsDummyDevice);
@ -153,9 +153,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestExitSongSelectWithClick() public void TestExitSongSelectWithClick()
{ {
TestSongSelect songSelect = null; TestPlaySongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestSongSelect()); PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show()); AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
AddStep("Move mouse to backButton", () => InputManager.MoveMouseTo(backButtonPosition)); AddStep("Move mouse to backButton", () => InputManager.MoveMouseTo(backButtonPosition));
@ -213,9 +213,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestModSelectInput() public void TestModSelectInput()
{ {
TestSongSelect songSelect = null; TestPlaySongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestSongSelect()); PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show()); AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show());
@ -234,9 +234,9 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestBeatmapOptionsInput() public void TestBeatmapOptionsInput()
{ {
TestSongSelect songSelect = null; TestPlaySongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestSongSelect()); PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddStep("Show options overlay", () => songSelect.BeatmapOptionsOverlay.Show()); AddStep("Show options overlay", () => songSelect.BeatmapOptionsOverlay.Show());
@ -312,11 +312,13 @@ namespace osu.Game.Tests.Visual.Navigation
ConfirmAtMainMenu(); ConfirmAtMainMenu();
} }
private class TestSongSelect : PlaySongSelect public class TestPlaySongSelect : PlaySongSelect
{ {
public ModSelectOverlay ModSelectOverlay => ModSelect; public ModSelectOverlay ModSelectOverlay => ModSelect;
public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions; public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions;
protected override bool DisplayStableImportPrompt => false;
} }
} }
} }

View File

@ -1,12 +1,16 @@
// 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.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; 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.Testing;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.AccountCreation;
using osu.Game.Overlays.Settings;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
@ -36,8 +40,6 @@ namespace osu.Game.Tests.Visual.Online
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
API.Logout();
localUser = API.LocalUser.GetBoundCopy(); localUser = API.LocalUser.GetBoundCopy();
localUser.BindValueChanged(user => { userPanelArea.Child = new UserGridPanel(user.NewValue) { Width = 200 }; }, true); localUser.BindValueChanged(user => { userPanelArea.Child = new UserGridPanel(user.NewValue) { Width = 200 }; }, true);
} }
@ -46,11 +48,14 @@ namespace osu.Game.Tests.Visual.Online
public void TestOverlayVisibility() public void TestOverlayVisibility()
{ {
AddStep("start hidden", () => accountCreation.Hide()); AddStep("start hidden", () => accountCreation.Hide());
AddStep("log out", API.Logout); AddStep("log out", () => API.Logout());
AddStep("show manually", () => accountCreation.Show()); AddStep("show manually", () => accountCreation.Show());
AddUntilStep("overlay is visible", () => accountCreation.State.Value == Visibility.Visible); AddUntilStep("overlay is visible", () => accountCreation.State.Value == Visibility.Visible);
AddStep("click button", () => accountCreation.ChildrenOfType<SettingsButton>().Single().Click());
AddUntilStep("warning screen is present", () => accountCreation.ChildrenOfType<ScreenWarning>().SingleOrDefault()?.IsPresent == true);
AddStep("log back in", () => API.Login("dummy", "password")); AddStep("log back in", () => API.Login("dummy", "password"));
AddUntilStep("overlay is hidden", () => accountCreation.State.Value == Visibility.Hidden); AddUntilStep("overlay is hidden", () => accountCreation.State.Value == Visibility.Hidden);
} }

View File

@ -19,8 +19,10 @@ namespace osu.Game.Tests.Visual.Online
{ {
public class TestSceneCurrentlyPlayingDisplay : OsuTestScene public class TestSceneCurrentlyPlayingDisplay : OsuTestScene
{ {
[Cached(typeof(SpectatorStreamingClient))] private readonly User streamingUser = new User { Id = 2, Username = "Test user" };
private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient();
[Cached(typeof(SpectatorClient))]
private TestSpectatorClient testSpectatorClient = new TestSpectatorClient();
private CurrentlyPlayingDisplay currentlyPlaying; private CurrentlyPlayingDisplay currentlyPlaying;
@ -34,7 +36,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
AddStep("add streaming client", () => AddStep("add streaming client", () =>
{ {
nestedContainer?.Remove(testSpectatorStreamingClient); nestedContainer?.Remove(testSpectatorClient);
Remove(lookupCache); Remove(lookupCache);
Children = new Drawable[] Children = new Drawable[]
@ -45,7 +47,7 @@ namespace osu.Game.Tests.Visual.Online
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
testSpectatorStreamingClient, testSpectatorClient,
currentlyPlaying = new CurrentlyPlayingDisplay currentlyPlaying = new CurrentlyPlayingDisplay
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -55,15 +57,15 @@ namespace osu.Game.Tests.Visual.Online
}; };
}); });
AddStep("Reset players", () => testSpectatorStreamingClient.PlayingUsers.Clear()); AddStep("Reset players", () => testSpectatorClient.EndPlay(streamingUser.Id));
} }
[Test] [Test]
public void TestBasicDisplay() public void TestBasicDisplay()
{ {
AddStep("Add playing user", () => testSpectatorStreamingClient.PlayingUsers.Add(2)); AddStep("Add playing user", () => testSpectatorClient.StartPlay(streamingUser.Id, 0));
AddUntilStep("Panel loaded", () => currentlyPlaying.ChildrenOfType<UserGridPanel>()?.FirstOrDefault()?.User.Id == 2); AddUntilStep("Panel loaded", () => currentlyPlaying.ChildrenOfType<UserGridPanel>()?.FirstOrDefault()?.User.Id == 2);
AddStep("Remove playing user", () => testSpectatorStreamingClient.PlayingUsers.Remove(2)); AddStep("Remove playing user", () => testSpectatorClient.EndPlay(streamingUser.Id));
AddUntilStep("Panel no longer present", () => !currentlyPlaying.ChildrenOfType<UserGridPanel>().Any()); AddUntilStep("Panel no longer present", () => !currentlyPlaying.ChildrenOfType<UserGridPanel>().Any());
} }

View File

@ -0,0 +1,152 @@
// 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.Allocation;
using osu.Framework.Testing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.News.Sidebar;
using static osu.Game.Overlays.News.Sidebar.YearsPanel;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneNewsSidebar : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
private TestNewsSidebar sidebar;
[SetUp]
public void SetUp() => Schedule(() => Child = sidebar = new TestNewsSidebar { YearChanged = onYearChanged });
[Test]
public void TestBasic()
{
AddStep("Add metadata", () => sidebar.Metadata.Value = getMetadata(2021));
AddUntilStep("Month sections exist", () => sidebar.ChildrenOfType<MonthSection>().Any());
}
[Test]
public void TestMetadataWithNoPosts()
{
AddStep("Add data with no posts", () => sidebar.Metadata.Value = metadata_with_no_posts);
AddUntilStep("No month sections were created", () => !sidebar.ChildrenOfType<MonthSection>().Any());
}
[Test]
public void TestYearsPanelVisibility()
{
AddUntilStep("Years panel is hidden", () => yearsPanel?.Alpha == 0);
AddStep("Add data", () => sidebar.Metadata.Value = getMetadata(2021));
AddUntilStep("Years panel is visible", () => yearsPanel?.Alpha == 1);
}
private void onYearChanged(int year) => sidebar.Metadata.Value = getMetadata(year);
private YearsPanel yearsPanel => sidebar.ChildrenOfType<YearsPanel>().FirstOrDefault();
private APINewsSidebar getMetadata(int year) => new APINewsSidebar
{
CurrentYear = year,
Years = new[]
{
2021,
2020,
2019,
2018,
2017,
2016,
2015,
2014,
2013
},
NewsPosts = new List<APINewsPost>
{
new APINewsPost
{
Title = "(Mar) Short title",
PublishedAt = new DateTime(year, 3, 1)
},
new APINewsPost
{
Title = "(Mar) Oh boy that's a long post title I wonder if it will break anything",
PublishedAt = new DateTime(year, 3, 1)
},
new APINewsPost
{
Title = "(Mar) Medium title, nothing to see here",
PublishedAt = new DateTime(year, 3, 1)
},
new APINewsPost
{
Title = "(Feb) Short title",
PublishedAt = new DateTime(year, 2, 1)
},
new APINewsPost
{
Title = "(Feb) Oh boy that's a long post title I wonder if it will break anything",
PublishedAt = new DateTime(year, 2, 1)
},
new APINewsPost
{
Title = "(Feb) Medium title, nothing to see here",
PublishedAt = new DateTime(year, 2, 1)
},
new APINewsPost
{
Title = "Short title",
PublishedAt = new DateTime(year, 1, 1)
},
new APINewsPost
{
Title = "Oh boy that's a long post title I wonder if it will break anything",
PublishedAt = new DateTime(year, 1, 1)
},
new APINewsPost
{
Title = "Medium title, nothing to see here",
PublishedAt = new DateTime(year, 1, 1)
}
}
};
private static readonly APINewsSidebar metadata_with_no_posts = new APINewsSidebar
{
CurrentYear = 2021,
Years = new[]
{
2021,
2020,
2019,
2018,
2017,
2016,
2015,
2014,
2013
},
NewsPosts = Array.Empty<APINewsPost>()
};
private class TestNewsSidebar : NewsSidebar
{
public Action<int> YearChanged;
protected override void LoadComplete()
{
base.LoadComplete();
Metadata.BindValueChanged(metadata =>
{
foreach (var b in this.ChildrenOfType<YearButton>())
b.Action = () => YearChanged?.Invoke(b.Year);
}, true);
}
}
}
}

View File

@ -0,0 +1,43 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays;
using osu.Game.Overlays.Wiki;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneWikiMainPage : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Orange);
public TestSceneWikiMainPage()
{
Children = new Drawable[]
{
new Box
{
Colour = overlayColour.Background5,
RelativeSizeAxes = Axes.Both,
},
new BasicScrollContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(20),
Child = new WikiMainPage
{
Markdown = main_page_markdown
}
}
};
}
// From https://osu.ppy.sh/api/v2/wiki/en/Main_Page
private const string main_page_markdown =
"---\nlayout: main_page\n---\n\n<!-- Do not add any empty lines inside this div. -->\n\n<div class=\"wiki-main-page__blurb\">\nWelcome to the osu! wiki, a project containing a wide range of osu! related information.\n</div>\n\n<div class=\"wiki-main-page__panels\">\n<div class=\"wiki-main-page-panel wiki-main-page-panel--full\">\n\n# Getting started\n\n[Welcome](/wiki/Welcome) • [Installation](/wiki/Installation) • [Registration](/wiki/Registration) • [Help Centre](/wiki/Help_Centre) • [FAQ](/wiki/FAQ)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# Game client\n\n[Interface](/wiki/Interface) • [Options](/wiki/Options) • [Visual settings](/wiki/Visual_Settings) • [Shortcut key reference](/wiki/Shortcut_key_reference) • [Configuration file](/wiki/osu!_Program_Files/User_Configuration_File) • [Program files](/wiki/osu!_Program_Files)\n\n[File formats](/wiki/osu!_File_Formats): [.osz](/wiki/osu!_File_Formats/Osz_(file_format)) • [.osk](/wiki/osu!_File_Formats/Osk_(file_format)) • [.osr](/wiki/osu!_File_Formats/Osr_(file_format)) • [.osu](/wiki/osu!_File_Formats/Osu_(file_format)) • [.osb](/wiki/osu!_File_Formats/Osb_(file_format)) • [.db](/wiki/osu!_File_Formats/Db_(file_format))\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# Gameplay\n\n[Game modes](/wiki/Game_mode): [osu!](/wiki/Game_mode/osu!) • [osu!taiko](/wiki/Game_mode/osu!taiko) • [osu!catch](/wiki/Game_mode/osu!catch) • [osu!mania](/wiki/Game_mode/osu!mania)\n\n[Beatmap](/wiki/Beatmap) • [Hit object](/wiki/Hit_object) • [Mods](/wiki/Game_modifier) • [Score](/wiki/Score) • [Replay](/wiki/Replay) • [Multi](/wiki/Multi)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# [Beatmap editor](/wiki/Beatmap_Editor)\n\nSections: [Compose](/wiki/Beatmap_Editor/Compose) • [Design](/wiki/Beatmap_Editor/Design) • [Timing](/wiki/Beatmap_Editor/Timing) • [Song setup](/wiki/Beatmap_Editor/Song_Setup)\n\nComponents: [AiMod](/wiki/Beatmap_Editor/AiMod) • [Beat snap divisor](/wiki/Beatmap_Editor/Beat_Snap_Divisor) • [Distance snap](/wiki/Beatmap_Editor/Distance_Snap) • [Menu](/wiki/Beatmap_Editor/Menu) • [SB load](/wiki/Beatmap_Editor/SB_Load) • [Timelines](/wiki/Beatmap_Editor/Timelines)\n\n[Beatmapping](/wiki/Beatmapping) • [Difficulty](/wiki/Beatmap/Difficulty) • [Mapping techniques](/wiki/Mapping_Techniques) • [Storyboarding](/wiki/Storyboarding)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# Beatmap submission and ranking\n\n[Submission](/wiki/Submission) • [Modding](/wiki/Modding) • [Ranking procedure](/wiki/Beatmap_ranking_procedure) • [Mappers' Guild](/wiki/Mappers_Guild) • [Project Loved](/wiki/Project_Loved)\n\n[Ranking criteria](/wiki/Ranking_Criteria): [osu!](/wiki/Ranking_Criteria/osu!) • [osu!taiko](/wiki/Ranking_Criteria/osu!taiko) • [osu!catch](/wiki/Ranking_Criteria/osu!catch) • [osu!mania](/wiki/Ranking_Criteria/osu!mania)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# Community\n\n[Tournaments](/wiki/Tournaments) • [Skinning](/wiki/Skinning) • [Projects](/wiki/Projects) • [Guides](/wiki/Guides) • [osu!dev Discord server](/wiki/osu!dev_Discord_server) • [How you can help](/wiki/How_You_Can_Help!) • [Glossary](/wiki/Glossary)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# People\n\n[The Team](/wiki/People/The_Team): [Developers](/wiki/People/The_Team/Developers) • [Global Moderation Team](/wiki/People/The_Team/Global_Moderation_Team) • [Support Team](/wiki/People/The_Team/Support_Team) • [Nomination Assessment Team](/wiki/People/The_Team/Nomination_Assessment_Team) • [Beatmap Nominators](/wiki/People/The_Team/Beatmap_Nominators) • [osu! Alumni](/wiki/People/The_Team/osu!_Alumni) • [Project Loved Team](/wiki/People/The_Team/Project_Loved_Team)\n\nOrganisations: [osu! UCI](/wiki/Organisations/osu!_UCI)\n\n[Community Contributors](/wiki/People/Community_Contributors) • [Users with unique titles](/wiki/People/Users_with_unique_titles)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# For developers\n\n[API](/wiki/osu!api) • [Bot account](/wiki/Bot_account) • [Brand identity guidelines](/wiki/Brand_identity_guidelines)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# About the wiki\n\n[Sitemap](/wiki/Sitemap) • [Contribution guide](/wiki/osu!_wiki_Contribution_Guide) • [Article styling criteria](/wiki/Article_Styling_Criteria) • [News styling criteria](/wiki/News_Styling_Criteria)\n\n</div>\n</div>\n";
}
}

View File

@ -0,0 +1,175 @@
// 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 Markdig.Syntax.Inlines;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers.Markdown;
using osu.Game.Overlays;
using osu.Game.Overlays.Wiki.Markdown;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneWikiMarkdownContainer : OsuTestScene
{
private TestMarkdownContainer markdownContainer;
[Cached]
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Orange);
[SetUp]
public void Setup() => Schedule(() =>
{
Children = new Drawable[]
{
new Box
{
Colour = overlayColour.Background5,
RelativeSizeAxes = Axes.Both,
},
new BasicScrollContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(20),
Child = markdownContainer = new TestMarkdownContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
}
};
});
[Test]
public void TestLink()
{
AddStep("set current path", () => markdownContainer.CurrentPath = "Article_styling_criteria/");
AddStep("set '/wiki/Main_Page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_Page)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Main_Page");
AddStep("set '../FAQ''", () => markdownContainer.Text = "[FAQ](../FAQ)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/FAQ");
AddStep("set './Writing''", () => markdownContainer.Text = "[wiki writing guidline](./Writing)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/Writing");
AddStep("set 'Formatting''", () => markdownContainer.Text = "[wiki formatting guidline](Formatting)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/Formatting");
}
[Test]
public void TestOutdatedNoticeBox()
{
AddStep("Add outdated yaml header", () =>
{
markdownContainer.Text = @"---
outdated: true
---";
});
}
[Test]
public void TestNeedsCleanupNoticeBox()
{
AddStep("Add needs cleanup yaml header", () =>
{
markdownContainer.Text = @"---
needs_cleanup: true
---";
});
}
[Test]
public void TestOnlyShowOutdatedNoticeBox()
{
AddStep("Add outdated and needs cleanup yaml", () =>
{
markdownContainer.Text = @"---
outdated: true
needs_cleanup: true
---";
});
}
[Test]
public void TestAbsoluteImage()
{
AddStep("Add absolute image", () =>
{
markdownContainer.DocumentUrl = "https://dev.ppy.sh";
markdownContainer.Text = "![intro](/wiki/Interface/img/intro-screen.jpg)";
});
}
[Test]
public void TestRelativeImage()
{
AddStep("Add relative image", () =>
{
markdownContainer.DocumentUrl = "https://dev.ppy.sh";
markdownContainer.CurrentPath = "Interface/";
markdownContainer.Text = "![intro](img/intro-screen.jpg)";
});
}
[Test]
public void TestBlockImage()
{
AddStep("Add paragraph with block image", () =>
{
markdownContainer.DocumentUrl = "https://dev.ppy.sh";
markdownContainer.CurrentPath = "Interface/";
markdownContainer.Text = @"Line before image
![play menu](img/play-menu.jpg ""Main Menu in osu!"")
Line after image";
});
}
[Test]
public void TestInlineImage()
{
AddStep("Add inline image", () =>
{
markdownContainer.DocumentUrl = "https://dev.ppy.sh";
markdownContainer.Text = "![osu! mode icon](/wiki/shared/mode/osu.png) osu!";
});
}
private class TestMarkdownContainer : WikiMarkdownContainer
{
public LinkInline Link;
public new string DocumentUrl
{
set => base.DocumentUrl = value;
}
public override MarkdownTextFlowContainer CreateTextFlow() => new TestMarkdownTextFlowContainer
{
UrlAdded = link => Link = link,
};
private class TestMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer
{
public Action<LinkInline> UrlAdded;
protected override void AddLinkText(string text, LinkInline linkInline)
{
base.AddLinkText(text, linkInline);
UrlAdded?.Invoke(linkInline);
}
protected override void AddImage(LinkInline linkInline) => AddDrawable(new WikiMarkdownImage(linkInline));
}
}
}
}

View File

@ -22,82 +22,17 @@ namespace osu.Game.Tests.Visual.Ranking
{ {
public class TestSceneAccuracyCircle : OsuTestScene public class TestSceneAccuracyCircle : OsuTestScene
{ {
[Test] [TestCase(0.2, ScoreRank.D)]
public void TestLowDRank() [TestCase(0.5, ScoreRank.D)]
[TestCase(0.75, ScoreRank.C)]
[TestCase(0.85, ScoreRank.B)]
[TestCase(0.925, ScoreRank.A)]
[TestCase(0.975, ScoreRank.S)]
[TestCase(0.9999, ScoreRank.S)]
[TestCase(1, ScoreRank.X)]
public void TestRank(double accuracy, ScoreRank rank)
{ {
var score = createScore(); var score = createScore(accuracy, rank);
score.Accuracy = 0.2;
score.Rank = ScoreRank.D;
addCircleStep(score);
}
[Test]
public void TestDRank()
{
var score = createScore();
score.Accuracy = 0.5;
score.Rank = ScoreRank.D;
addCircleStep(score);
}
[Test]
public void TestCRank()
{
var score = createScore();
score.Accuracy = 0.75;
score.Rank = ScoreRank.C;
addCircleStep(score);
}
[Test]
public void TestBRank()
{
var score = createScore();
score.Accuracy = 0.85;
score.Rank = ScoreRank.B;
addCircleStep(score);
}
[Test]
public void TestARank()
{
var score = createScore();
score.Accuracy = 0.925;
score.Rank = ScoreRank.A;
addCircleStep(score);
}
[Test]
public void TestSRank()
{
var score = createScore();
score.Accuracy = 0.975;
score.Rank = ScoreRank.S;
addCircleStep(score);
}
[Test]
public void TestAlmostSSRank()
{
var score = createScore();
score.Accuracy = 0.9999;
score.Rank = ScoreRank.S;
addCircleStep(score);
}
[Test]
public void TestSSRank()
{
var score = createScore();
score.Accuracy = 1;
score.Rank = ScoreRank.X;
addCircleStep(score); addCircleStep(score);
} }
@ -120,7 +55,7 @@ namespace osu.Game.Tests.Visual.Ranking
} }
} }
}, },
new AccuracyCircle(score, true) new AccuracyCircle(score)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -129,7 +64,7 @@ namespace osu.Game.Tests.Visual.Ranking
}; };
}); });
private ScoreInfo createScore() => new ScoreInfo private ScoreInfo createScore(double accuracy, ScoreRank rank) => new ScoreInfo
{ {
User = new User User = new User
{ {
@ -139,9 +74,9 @@ namespace osu.Game.Tests.Visual.Ranking
Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo,
Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
TotalScore = 2845370, TotalScore = 2845370,
Accuracy = 0.95, Accuracy = accuracy,
MaxCombo = 999, MaxCombo = 999,
Rank = ScoreRank.S, Rank = rank,
Date = DateTimeOffset.Now, Date = DateTimeOffset.Now,
Statistics = Statistics =
{ {

View File

@ -29,13 +29,8 @@ namespace osu.Game.Tests.Visual.Ranking
[TestFixture] [TestFixture]
public class TestSceneResultsScreen : OsuManualInputManagerTestScene public class TestSceneResultsScreen : OsuManualInputManagerTestScene
{ {
private BeatmapManager beatmaps; [Resolved]
private BeatmapManager beatmaps { get; set; }
[BackgroundDependencyLoader]
private void load(BeatmapManager beatmaps)
{
this.beatmaps = beatmaps;
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
@ -46,10 +41,6 @@ namespace osu.Game.Tests.Visual.Ranking
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
} }
private TestResultsScreen createResultsScreen() => new TestResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo));
private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo));
[Test] [Test]
public void TestResultsWithoutPlayer() public void TestResultsWithoutPlayer()
{ {
@ -69,12 +60,25 @@ namespace osu.Game.Tests.Visual.Ranking
AddAssert("retry overlay not present", () => screen.RetryOverlay == null); AddAssert("retry overlay not present", () => screen.RetryOverlay == null);
} }
[Test] [TestCase(0.2, ScoreRank.D)]
public void TestResultsWithPlayer() [TestCase(0.5, ScoreRank.D)]
[TestCase(0.75, ScoreRank.C)]
[TestCase(0.85, ScoreRank.B)]
[TestCase(0.925, ScoreRank.A)]
[TestCase(0.975, ScoreRank.S)]
[TestCase(0.9999, ScoreRank.S)]
[TestCase(1, ScoreRank.X)]
public void TestResultsWithPlayer(double accuracy, ScoreRank rank)
{ {
TestResultsScreen screen = null; TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); var score = new TestScoreInfo(new OsuRuleset().RulesetInfo)
{
Accuracy = accuracy,
Rank = rank
};
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen(score)));
AddUntilStep("wait for loaded", () => screen.IsLoaded); AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay present", () => screen.RetryOverlay != null); AddAssert("retry overlay present", () => screen.RetryOverlay != null);
} }
@ -232,6 +236,10 @@ namespace osu.Game.Tests.Visual.Ranking
AddAssert("download button is enabled", () => screen.ChildrenOfType<DownloadButton>().Last().Enabled.Value); AddAssert("download button is enabled", () => screen.ChildrenOfType<DownloadButton>().Last().Enabled.Value);
} }
private TestResultsScreen createResultsScreen(ScoreInfo score = null) => new TestResultsScreen(score ?? new TestScoreInfo(new OsuRuleset().RulesetInfo));
private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo));
private class TestResultsContainer : Container private class TestResultsContainer : Container
{ {
[Cached(typeof(Player))] [Cached(typeof(Player))]

View File

@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.KeyBinding; using osu.Game.Overlays.KeyBinding;
using osuTK.Input; using osuTK.Input;
@ -28,6 +29,39 @@ namespace osu.Game.Tests.Visual.Settings
panel.Show(); panel.Show();
} }
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Scroll to top", () => panel.ChildrenOfType<SettingsPanel.SettingsSectionsContainer>().First().ScrollToTop());
AddWaitStep("wait for scroll", 5);
}
[Test]
public void TestBindingMouseWheelToNonGameplay()
{
scrollToAndStartBinding("Increase volume");
AddStep("press k", () => InputManager.Key(Key.K));
checkBinding("Increase volume", "K");
AddStep("click again", () => InputManager.Click(MouseButton.Left));
AddStep("scroll mouse wheel", () => InputManager.ScrollVerticalBy(1));
checkBinding("Increase volume", "Wheel Up");
}
[Test]
public void TestBindingMouseWheelToGameplay()
{
scrollToAndStartBinding("Left button");
AddStep("press k", () => InputManager.Key(Key.Z));
checkBinding("Left button", "Z");
AddStep("click again", () => InputManager.Click(MouseButton.Left));
AddStep("scroll mouse wheel", () => InputManager.ScrollVerticalBy(1));
checkBinding("Left button", "Z");
}
[Test] [Test]
public void TestClickTwiceOnClearButton() public void TestClickTwiceOnClearButton()
{ {
@ -36,6 +70,7 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("click first row", () => AddStep("click first row", () =>
{ {
firstRow = panel.ChildrenOfType<KeyBindingRow>().First(); firstRow = panel.ChildrenOfType<KeyBindingRow>().First();
InputManager.MoveMouseTo(firstRow); InputManager.MoveMouseTo(firstRow);
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
@ -104,6 +139,64 @@ namespace osu.Game.Tests.Visual.Settings
} }
} }
[Test]
public void TestSingleBindingResetButton()
{
KeyBindingRow settingsKeyBindingRow = null;
AddStep("click first row", () =>
{
settingsKeyBindingRow = panel.ChildrenOfType<KeyBindingRow>().First();
InputManager.MoveMouseTo(settingsKeyBindingRow);
InputManager.Click(MouseButton.Left);
InputManager.PressKey(Key.P);
InputManager.ReleaseKey(Key.P);
});
AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First().Alpha > 0);
AddStep("click reset button for bindings", () =>
{
var resetButton = settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First();
resetButton.Click();
});
AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First().Alpha == 0);
AddAssert("binding cleared", () => settingsKeyBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0)));
}
[Test]
public void TestResetAllBindingsButton()
{
KeyBindingRow settingsKeyBindingRow = null;
AddStep("click first row", () =>
{
settingsKeyBindingRow = panel.ChildrenOfType<KeyBindingRow>().First();
InputManager.MoveMouseTo(settingsKeyBindingRow);
InputManager.Click(MouseButton.Left);
InputManager.PressKey(Key.P);
InputManager.ReleaseKey(Key.P);
});
AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First().Alpha > 0);
AddStep("click reset button for bindings", () =>
{
var resetButton = panel.ChildrenOfType<ResetButton>().First();
resetButton.Click();
});
AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First().Alpha == 0);
AddAssert("binding cleared", () => settingsKeyBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0)));
}
[Test] [Test]
public void TestClickRowSelectsFirstBinding() public void TestClickRowSelectsFirstBinding()
{ {
@ -135,5 +228,37 @@ namespace osu.Game.Tests.Visual.Settings
AddAssert("first binding selected", () => multiBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().First().IsBinding); AddAssert("first binding selected", () => multiBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().First().IsBinding);
} }
private void checkBinding(string name, string keyName)
{
AddAssert($"Check {name} is bound to {keyName}", () =>
{
var firstRow = panel.ChildrenOfType<KeyBindingRow>().First(r => r.ChildrenOfType<OsuSpriteText>().Any(s => s.Text == name));
var firstButton = firstRow.ChildrenOfType<KeyBindingRow.KeyButton>().First();
return firstButton.Text.Text == keyName;
});
}
private void scrollToAndStartBinding(string name)
{
KeyBindingRow.KeyButton firstButton = null;
AddStep($"Scroll to {name}", () =>
{
var firstRow = panel.ChildrenOfType<KeyBindingRow>().First(r => r.ChildrenOfType<OsuSpriteText>().Any(s => s.Text == name));
firstButton = firstRow.ChildrenOfType<KeyBindingRow.KeyButton>().First();
panel.ChildrenOfType<SettingsPanel.SettingsSectionsContainer>().First().ScrollTo(firstButton);
});
AddWaitStep("wait for scroll", 5);
AddStep("click to bind", () =>
{
InputManager.MoveMouseTo(firstButton);
InputManager.Click(MouseButton.Left);
});
}
} }
} }

View File

@ -7,6 +7,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.Settings namespace osu.Game.Tests.Visual.Settings
{ {
@ -37,7 +38,7 @@ namespace osu.Game.Tests.Visual.Settings
private class TestSettingsTextBox : SettingsTextBox private class TestSettingsTextBox : SettingsTextBox
{ {
public new Drawable RestoreDefaultValueButton => this.ChildrenOfType<RestoreDefaultValueButton>().Single(); public Drawable RestoreDefaultValueButton => this.ChildrenOfType<RestoreDefaultValueButton<string>>().Single();
} }
} }
} }

View File

@ -786,9 +786,12 @@ namespace osu.Game.Tests.Visual.SongSelect
} }
} }
private void checkVisibleItemCount(bool diff, int count) => private void checkVisibleItemCount(bool diff, int count)
AddAssert($"{count} {(diff ? "diffs" : "sets")} visible", () => {
// until step required as we are querying against alive items, which are loaded asynchronously inside DrawableCarouselBeatmapSet.
AddUntilStep($"{count} {(diff ? "diffs" : "sets")} visible", () =>
carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible) == count); carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible) == count);
}
private void checkNoSelection() => AddAssert("Selection is null", () => currentSelection == null); private void checkNoSelection() => AddAssert("Selection is null", () => currentSelection == null);

View File

@ -358,7 +358,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target)); AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target));
// this is an important check, to make sure updateComponentFromBeatmap() was actually run // this is an important check, to make sure updateComponentFromBeatmap() was actually run
AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo == target); AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo.Equals(target));
} }
[Test] [Test]
@ -390,7 +390,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("has correct ruleset", () => Ruleset.Value.ID == 0); AddUntilStep("has correct ruleset", () => Ruleset.Value.ID == 0);
// this is an important check, to make sure updateComponentFromBeatmap() was actually run // this is an important check, to make sure updateComponentFromBeatmap() was actually run
AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo == target); AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo.Equals(target));
} }
[Test] [Test]
@ -781,7 +781,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3); AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3);
AddAssert("Check first item in group selected", () => Beatmap.Value.BeatmapInfo == groupIcon.Items.First().Beatmap); AddAssert("Check first item in group selected", () => Beatmap.Value.BeatmapInfo.Equals(groupIcon.Items.First().Beatmap));
} }
[Test] [Test]

View File

@ -0,0 +1,86 @@
// 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.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneBreadcrumbControlHeader : OsuTestScene
{
private static readonly string[] items = { "first", "second", "third", "fourth", "fifth" };
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Red);
private TestHeader header;
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = header = new TestHeader
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
});
[Test]
public void TestAddAndRemoveItem()
{
foreach (var item in items.Skip(1))
AddStep($"Add {item} item", () => header.AddItem(item));
foreach (var item in items.Reverse().SkipLast(3))
AddStep($"Remove {item} item", () => header.RemoveItem(item));
AddStep("Clear items", () => header.ClearItems());
foreach (var item in items)
AddStep($"Add {item} item", () => header.AddItem(item));
foreach (var item in items)
AddStep($"Remove {item} item", () => header.RemoveItem(item));
}
private class TestHeader : BreadcrumbControlOverlayHeader
{
public TestHeader()
{
TabControl.AddItem(items[0]);
Current.Value = items[0];
}
public void AddItem(string value)
{
TabControl.AddItem(value);
Current.Value = TabControl.Items.LastOrDefault();
}
public void RemoveItem(string value)
{
TabControl.RemoveItem(value);
Current.Value = TabControl.Items.LastOrDefault();
}
public void ClearItems()
{
TabControl.Clear();
Current.Value = null;
}
protected override OverlayTitle CreateTitle() => new TestTitle();
}
private class TestTitle : OverlayTitle
{
public TestTitle()
{
Title = "Test Title";
}
}
}
}

View File

@ -2,7 +2,9 @@
// 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 NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
@ -11,13 +13,20 @@ namespace osu.Game.Tests.Visual.UserInterface
[TestFixture] [TestFixture]
public class TestSceneDialogOverlay : OsuTestScene public class TestSceneDialogOverlay : OsuTestScene
{ {
public TestSceneDialogOverlay() private DialogOverlay overlay;
[SetUpSteps]
public void SetUpSteps()
{ {
DialogOverlay overlay; AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
}
Add(overlay = new DialogOverlay()); [Test]
public void TestBasic()
{
TestPopupDialog dialog = null;
AddStep("dialog #1", () => overlay.Push(new TestPopupDialog AddStep("dialog #1", () => overlay.Push(dialog = new TestPopupDialog
{ {
Icon = FontAwesome.Regular.TrashAlt, Icon = FontAwesome.Regular.TrashAlt,
HeaderText = @"Confirm deletion of", HeaderText = @"Confirm deletion of",
@ -37,7 +46,9 @@ namespace osu.Game.Tests.Visual.UserInterface
}, },
})); }));
AddStep("dialog #2", () => overlay.Push(new TestPopupDialog AddAssert("first dialog displayed", () => overlay.CurrentDialog == dialog);
AddStep("dialog #2", () => overlay.Push(dialog = new TestPopupDialog
{ {
Icon = FontAwesome.Solid.Cog, Icon = FontAwesome.Solid.Cog,
HeaderText = @"What do you want to do with", HeaderText = @"What do you want to do with",
@ -70,6 +81,42 @@ namespace osu.Game.Tests.Visual.UserInterface
}, },
}, },
})); }));
AddAssert("second dialog displayed", () => overlay.CurrentDialog == dialog);
}
[Test]
public void TestDismissBeforePush()
{
AddStep("dismissed dialog push", () =>
{
overlay.Push(new TestPopupDialog
{
State = { Value = Visibility.Hidden }
});
});
AddAssert("no dialog pushed", () => overlay.CurrentDialog == null);
}
[Test]
public void TestDismissBeforePushViaButtonPress()
{
AddStep("dismissed dialog push", () =>
{
TestPopupDialog dialog;
overlay.Push(dialog = new TestPopupDialog
{
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton { Text = @"OK" },
},
});
dialog.PerformOkAction();
});
AddAssert("no dialog pushed", () => overlay.CurrentDialog == null);
} }
private class TestPopupDialog : PopupDialog private class TestPopupDialog : PopupDialog

View File

@ -86,6 +86,15 @@ _**italic with underscore, bold with asterisk**_";
}); });
} }
[Test]
public void TestLinkWithTitle()
{
AddStep("Add Link with title", () =>
{
markdownContainer.Text = "[wikipedia](https://www.wikipedia.org \"The Free Encyclopedia\")";
});
}
[Test] [Test]
public void TestInlineCode() public void TestInlineCode()
{ {

View File

@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public class TestSceneStatefulMenuItem : OsuManualInputManagerTestScene public class TestSceneStatefulMenuItem : OsuManualInputManagerTestScene
{ {
[Test] [Test]
public void TestTernaryMenuItem() public void TestTernaryRadioMenuItem()
{ {
OsuMenu menu = null; OsuMenu menu = null;
@ -30,9 +30,57 @@ namespace osu.Game.Tests.Visual.UserInterface
Origin = Anchor.Centre, Origin = Anchor.Centre,
Items = new[] Items = new[]
{ {
new TernaryStateMenuItem("First"), new TernaryStateRadioMenuItem("First"),
new TernaryStateMenuItem("Second") { State = { BindTarget = state } }, new TernaryStateRadioMenuItem("Second") { State = { BindTarget = state } },
new TernaryStateMenuItem("Third") { State = { Value = TernaryState.True } }, new TernaryStateRadioMenuItem("Third") { State = { Value = TernaryState.True } },
}
};
});
checkState(TernaryState.Indeterminate);
click();
checkState(TernaryState.True);
click();
checkState(TernaryState.True);
click();
checkState(TernaryState.True);
AddStep("change state via bindable", () => state.Value = TernaryState.True);
void click() =>
AddStep("click", () =>
{
InputManager.MoveMouseTo(menu.ScreenSpaceDrawQuad.Centre);
InputManager.Click(MouseButton.Left);
});
void checkState(TernaryState expected)
=> AddAssert($"state is {expected}", () => state.Value == expected);
}
[Test]
public void TestTernaryToggleMenuItem()
{
OsuMenu menu = null;
Bindable<TernaryState> state = new Bindable<TernaryState>(TernaryState.Indeterminate);
AddStep("create menu", () =>
{
state.Value = TernaryState.Indeterminate;
Child = menu = new OsuMenu(Direction.Vertical, true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Items = new[]
{
new TernaryStateToggleMenuItem("First"),
new TernaryStateToggleMenuItem("Second") { State = { BindTarget = state } },
new TernaryStateToggleMenuItem("Third") { State = { Value = TernaryState.True } },
} }
}; };
}); });

View File

@ -0,0 +1,183 @@
// 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 System.Threading;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics.Containers;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneUpdateableBeatmapSetCover : OsuTestScene
{
[Test]
public void TestLocal([Values] BeatmapSetCoverType coverType)
{
AddStep("setup cover", () => Child = new UpdateableBeatmapSetCover(coverType)
{
BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet,
RelativeSizeAxes = Axes.Both,
Masking = true,
});
AddUntilStep("wait for load", () => this.ChildrenOfType<BeatmapSetCover>().SingleOrDefault()?.IsLoaded ?? false);
}
[Test]
public void TestUnloadAndReload()
{
OsuScrollContainer scroll = null;
List<UpdateableBeatmapSetCover> covers = new List<UpdateableBeatmapSetCover>();
AddStep("setup covers", () =>
{
BeatmapSetInfo setInfo = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet;
FillFlowContainer fillFlow;
Child = scroll = new OsuScrollContainer
{
Size = new Vector2(500f),
Child = fillFlow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Padding = new MarginPadding { Bottom = 550 }
}
};
var coverTypes = Enum.GetValues(typeof(BeatmapSetCoverType))
.Cast<BeatmapSetCoverType>()
.ToList();
for (int i = 0; i < 25; i++)
{
var coverType = coverTypes[i % coverTypes.Count];
var cover = new UpdateableBeatmapSetCover(coverType)
{
BeatmapSet = setInfo,
Height = 100,
Masking = true,
};
if (coverType == BeatmapSetCoverType.Cover)
cover.Width = 500;
else if (coverType == BeatmapSetCoverType.Card)
cover.Width = 400;
else if (coverType == BeatmapSetCoverType.List)
cover.Size = new Vector2(100, 50);
fillFlow.Add(cover);
covers.Add(cover);
}
});
var loadedCovers = covers.Where(c => c.ChildrenOfType<BeatmapSetCover>().SingleOrDefault()?.IsLoaded ?? false);
AddUntilStep("some loaded", () => loadedCovers.Any());
AddStep("scroll to end", () => scroll.ScrollToEnd());
AddUntilStep("all unloaded", () => !loadedCovers.Any());
}
[Test]
public void TestSetNullBeatmapWhileLoading()
{
TestUpdateableBeatmapSetCover updateableCover = null;
AddStep("setup cover", () => Child = updateableCover = new TestUpdateableBeatmapSetCover
{
BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet,
RelativeSizeAxes = Axes.Both,
Masking = true,
});
AddStep("change model", () => updateableCover.BeatmapSet = null);
AddWaitStep("wait some", 5);
AddAssert("no cover added", () => !updateableCover.ChildrenOfType<DelayedLoadUnloadWrapper>().Any());
}
[Test]
public void TestCoverChangeOnNewBeatmap()
{
TestUpdateableBeatmapSetCover updateableCover = null;
BeatmapSetCover initialCover = null;
AddStep("setup cover", () => Child = updateableCover = new TestUpdateableBeatmapSetCover(0)
{
BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg"),
RelativeSizeAxes = Axes.Both,
Masking = true,
Alpha = 0.4f
});
AddUntilStep("cover loaded", () => updateableCover.ChildrenOfType<BeatmapSetCover>().Any());
AddStep("store initial cover", () => initialCover = updateableCover.ChildrenOfType<BeatmapSetCover>().Single());
AddUntilStep("wait for fade complete", () => initialCover.Alpha == 1);
AddStep("switch beatmap",
() => updateableCover.BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg"));
AddUntilStep("new cover loaded", () => updateableCover.ChildrenOfType<BeatmapSetCover>().Except(new[] { initialCover }).Any());
}
private static BeatmapSetInfo createBeatmapWithCover(string coverUrl) => new BeatmapSetInfo
{
OnlineInfo = new BeatmapSetOnlineInfo
{
Covers = new BeatmapSetOnlineCovers { Cover = coverUrl }
}
};
private class TestUpdateableBeatmapSetCover : UpdateableBeatmapSetCover
{
private readonly int loadDelay;
public TestUpdateableBeatmapSetCover(int loadDelay = 10000)
{
this.loadDelay = loadDelay;
}
protected override Drawable CreateDrawable(BeatmapSetInfo model)
{
if (model == null)
return null;
return new TestBeatmapSetCover(model, loadDelay)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
};
}
}
private class TestBeatmapSetCover : BeatmapSetCover
{
private readonly int loadDelay;
public TestBeatmapSetCover(BeatmapSetInfo set, int loadDelay)
: base(set)
{
this.loadDelay = loadDelay;
}
[BackgroundDependencyLoader]
private void load()
{
Thread.Sleep(loadDelay);
}
}
}
}

View File

@ -11,6 +11,7 @@ using osu.Game.Beatmaps;
using osu.Game.IO.Archives; using osu.Game.IO.Archives;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Skinning;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
@ -52,6 +53,8 @@ namespace osu.Game.Tests
protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile)); protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile));
protected override ISkin GetSkin() => null;
public override Stream GetStream(string storagePath) => null; public override Stream GetStream(string storagePath) => null;
protected override Track GetBeatmapTrack() => trackStore.Get(firstAudioFile); protected override Track GetBeatmapTrack() => trackStore.Get(firstAudioFile);

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
@ -244,6 +245,8 @@ namespace osu.Game.Beatmaps
{ {
var setInfo = info.BeatmapSet; var setInfo = info.BeatmapSet;
Debug.Assert(setInfo.Files != null);
using (var stream = new MemoryStream()) using (var stream = new MemoryStream())
{ {
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
@ -290,7 +293,9 @@ namespace osu.Game.Beatmaps
if (beatmapInfo?.BeatmapSet == null || beatmapInfo == DefaultBeatmap?.BeatmapInfo) if (beatmapInfo?.BeatmapSet == null || beatmapInfo == DefaultBeatmap?.BeatmapInfo)
return DefaultBeatmap; return DefaultBeatmap;
if (beatmapInfo.BeatmapSet.Files == null) // force a re-query if files are not in a state which looks like the model has
// full database information present.
if (beatmapInfo.BeatmapSet.Files == null || beatmapInfo.BeatmapSet.Files.Count == 0)
{ {
var info = beatmapInfo; var info = beatmapInfo;
beatmapInfo = QueryBeatmap(b => b.ID == info.ID); beatmapInfo = QueryBeatmap(b => b.ID == info.ID);
@ -526,6 +531,7 @@ namespace osu.Game.Beatmaps
protected override IBeatmap GetBeatmap() => beatmap; protected override IBeatmap GetBeatmap() => beatmap;
protected override Texture GetBackground() => null; protected override Texture GetBackground() => null;
protected override Track GetBeatmapTrack() => null; protected override Track GetBeatmapTrack() => null;
protected override ISkin GetSkin() => null;
public override Stream GetStream(string storagePath) => null; public override Stream GetStream(string storagePath) => null;
} }
} }

View File

@ -1,6 +1,7 @@
// 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 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;
@ -8,78 +9,52 @@ using osu.Game.Graphics;
namespace osu.Game.Beatmaps.Drawables namespace osu.Game.Beatmaps.Drawables
{ {
public class UpdateableBeatmapSetCover : Container public class UpdateableBeatmapSetCover : ModelBackedDrawable<BeatmapSetInfo>
{ {
private Drawable displayedCover; private readonly BeatmapSetCoverType coverType;
private BeatmapSetInfo beatmapSet;
public BeatmapSetInfo BeatmapSet public BeatmapSetInfo BeatmapSet
{ {
get => beatmapSet; get => Model;
set set => Model = value;
{
if (value == beatmapSet) return;
beatmapSet = value;
if (IsLoaded)
updateCover();
}
} }
private BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover; public new bool Masking
public BeatmapSetCoverType CoverType
{ {
get => coverType; get => base.Masking;
set set => base.Masking = value;
{
if (value == coverType) return;
coverType = value;
if (IsLoaded)
updateCover();
}
} }
public UpdateableBeatmapSetCover() public UpdateableBeatmapSetCover(BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover)
{ {
Child = new Box this.coverType = coverType;
InternalChild = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.2f), Colour = OsuColour.Gray(0.2f),
}; };
} }
protected override void LoadComplete() protected override double LoadDelay => 500;
{
base.LoadComplete();
updateCover();
}
private void updateCover() protected override double TransformDuration => 400;
{
displayedCover?.FadeOut(400);
displayedCover?.Expire();
displayedCover = null;
if (beatmapSet != null) protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad)
=> new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad);
protected override Drawable CreateDrawable(BeatmapSetInfo model)
{ {
Add(displayedCover = new DelayedLoadUnloadWrapper(() => if (model == null)
{ return null;
var cover = new BeatmapSetCover(beatmapSet, coverType)
return new BeatmapSetCover(model, coverType)
{ {
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill, FillMode = FillMode.Fill,
}; };
cover.OnLoadComplete += d => d.FadeInFromZero(400, Easing.Out);
return cover;
}));
}
} }
} }
} }

View File

@ -15,6 +15,7 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
@ -49,6 +50,8 @@ namespace osu.Game.Beatmaps
protected override Track GetBeatmapTrack() => GetVirtualTrack(); protected override Track GetBeatmapTrack() => GetVirtualTrack();
protected override ISkin GetSkin() => null;
public override Stream GetStream(string storagePath) => null; public override Stream GetStream(string storagePath) => null;
private class DummyRulesetInfo : RulesetInfo private class DummyRulesetInfo : RulesetInfo

View File

@ -324,7 +324,7 @@ namespace osu.Game.Beatmaps
public bool SkinLoaded => skin.IsResultAvailable; public bool SkinLoaded => skin.IsResultAvailable;
public ISkin Skin => skin.Value; public ISkin Skin => skin.Value;
protected virtual ISkin GetSkin() => new DefaultSkin(null); protected abstract ISkin GetSkin();
private readonly RecyclableLazy<ISkin> skin; private readonly RecyclableLazy<ISkin> skin;
public abstract Stream GetStream(string storagePath); public abstract Stream GetStream(string storagePath);

View File

@ -8,13 +8,13 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.IO;
using osu.Game.IO.Legacy; using osu.Game.IO.Legacy;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
@ -38,8 +38,6 @@ namespace osu.Game.Collections
public readonly BindableList<BeatmapCollection> Collections = new BindableList<BeatmapCollection>(); public readonly BindableList<BeatmapCollection> Collections = new BindableList<BeatmapCollection>();
public bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
[Resolved] [Resolved]
private GameHost host { get; set; } private GameHost host { get; set; }
@ -60,8 +58,13 @@ namespace osu.Game.Collections
if (storage.Exists(database_name)) if (storage.Exists(database_name))
{ {
List<BeatmapCollection> beatmapCollections;
using (var stream = storage.GetStream(database_name)) using (var stream = storage.GetStream(database_name))
importCollections(readCollections(stream)); beatmapCollections = readCollections(stream);
// intentionally fire-and-forget async.
importCollections(beatmapCollections);
} }
} }
@ -96,25 +99,12 @@ namespace osu.Game.Collections
/// </summary> /// </summary>
public Action<Notification> PostNotification { protected get; set; } public Action<Notification> PostNotification { protected get; set; }
/// <summary>
/// Set a storage with access to an osu-stable install for import purposes.
/// </summary>
public Func<Storage> GetStableStorage { private get; set; }
/// <summary> /// <summary>
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
/// </summary> /// </summary>
public Task ImportFromStableAsync() public Task ImportFromStableAsync(StableStorage stableStorage)
{ {
var stable = GetStableStorage?.Invoke(); if (!stableStorage.Exists(database_name))
if (stable == null)
{
Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error);
return Task.CompletedTask;
}
if (!stable.Exists(database_name))
{ {
// This handles situations like when the user does not have a collections.db file // This handles situations like when the user does not have a collections.db file
Logger.Log($"No {database_name} available in osu!stable installation", LoggingTarget.Information, LogLevel.Error); Logger.Log($"No {database_name} available in osu!stable installation", LoggingTarget.Information, LogLevel.Error);
@ -123,7 +113,7 @@ namespace osu.Game.Collections
return Task.Run(async () => return Task.Run(async () =>
{ {
using (var stream = stable.GetStream(database_name)) using (var stream = stableStorage.GetStream(database_name))
await Import(stream).ConfigureAwait(false); await Import(stream).ConfigureAwait(false);
}); });
} }

View File

@ -104,7 +104,6 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.KeyOverlay, false); SetDefault(OsuSetting.KeyOverlay, false);
SetDefault(OsuSetting.PositionalHitSounds, true); SetDefault(OsuSetting.PositionalHitSounds, true);
SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true); SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true);
SetDefault(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth);
SetDefault(OsuSetting.FloatingComments, false); SetDefault(OsuSetting.FloatingComments, false);
@ -213,7 +212,6 @@ namespace osu.Game.Configuration
KeyOverlay, KeyOverlay,
PositionalHitSounds, PositionalHitSounds,
AlwaysPlayFirstComboBreak, AlwaysPlayFirstComboBreak,
ScoreMeter,
FloatingComments, FloatingComments,
HUDVisibilityMode, HUDVisibilityMode,
ShowProgressGraph, ShowProgressGraph,

View File

@ -1,37 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.ComponentModel;
namespace osu.Game.Configuration
{
public enum ScoreMeterType
{
[Description("None")]
None,
[Description("Hit Error (left)")]
HitErrorLeft,
[Description("Hit Error (right)")]
HitErrorRight,
[Description("Hit Error (left+right)")]
HitErrorBoth,
[Description("Hit Error (bottom)")]
HitErrorBottom,
[Description("Colour (left)")]
ColourLeft,
[Description("Colour (right)")]
ColourRight,
[Description("Colour (left+right)")]
ColourBoth,
[Description("Colour (bottom)")]
ColourBottom,
}
}

View File

@ -10,7 +10,6 @@ using System.Threading.Tasks;
using Humanizer; using Humanizer;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using osu.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
@ -81,8 +80,6 @@ namespace osu.Game.Database
public virtual IEnumerable<string> HandledExtensions => new[] { ".zip" }; public virtual IEnumerable<string> HandledExtensions => new[] { ".zip" };
public virtual bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
protected readonly FileStore Files; protected readonly FileStore Files;
protected readonly IDatabaseContextFactory ContextFactory; protected readonly IDatabaseContextFactory ContextFactory;
@ -669,16 +666,6 @@ namespace osu.Game.Database
#region osu-stable import #region osu-stable import
/// <summary>
/// Set a storage with access to an osu-stable install for import purposes.
/// </summary>
public Func<StableStorage> GetStableStorage { private get; set; }
/// <summary>
/// Denotes whether an osu-stable installation is present to perform automated imports from.
/// </summary>
public bool StableInstallationAvailable => GetStableStorage?.Invoke() != null;
/// <summary> /// <summary>
/// The relative path from osu-stable's data directory to import items from. /// The relative path from osu-stable's data directory to import items from.
/// </summary> /// </summary>
@ -700,22 +687,16 @@ namespace osu.Game.Database
/// <summary> /// <summary>
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
/// </summary> /// </summary>
public Task ImportFromStableAsync() public Task ImportFromStableAsync(StableStorage stableStorage)
{ {
var stableStorage = GetStableStorage?.Invoke();
if (stableStorage == null)
{
Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error);
return Task.CompletedTask;
}
var storage = PrepareStableStorage(stableStorage); var storage = PrepareStableStorage(stableStorage);
// Handle situations like when the user does not have a Skins folder.
if (!storage.ExistsDirectory(ImportFromStablePath)) if (!storage.ExistsDirectory(ImportFromStablePath))
{ {
// This handles situations like when the user does not have a Skins folder string fullPath = storage.GetFullPath(ImportFromStablePath);
Logger.Log($"No {ImportFromStablePath} folder available in osu!stable installation", LoggingTarget.Information, LogLevel.Error);
Logger.Log($"Folder \"{fullPath}\" not available in the target osu!stable installation to import {HumanisedModelName}s.", LoggingTarget.Information, LogLevel.Error);
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@ -0,0 +1,96 @@
// 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.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.IO;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings.Sections.Maintenance;
using osu.Game.Scoring;
using osu.Game.Skinning;
namespace osu.Game.Database
{
public class StableImportManager : Component
{
[Resolved]
private SkinManager skins { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
[Resolved]
private ScoreManager scores { get; set; }
[Resolved]
private CollectionManager collections { get; set; }
[Resolved]
private OsuGame game { get; set; }
[Resolved]
private DialogOverlay dialogOverlay { get; set; }
[Resolved(CanBeNull = true)]
private DesktopGameHost desktopGameHost { get; set; }
private StableStorage cachedStorage;
public bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
public async Task ImportFromStableAsync(StableContent content)
{
var stableStorage = await getStableStorage().ConfigureAwait(false);
var importTasks = new List<Task>();
Task beatmapImportTask = Task.CompletedTask;
if (content.HasFlagFast(StableContent.Beatmaps))
importTasks.Add(beatmapImportTask = beatmaps.ImportFromStableAsync(stableStorage));
if (content.HasFlagFast(StableContent.Skins))
importTasks.Add(skins.ImportFromStableAsync(stableStorage));
if (content.HasFlagFast(StableContent.Collections))
importTasks.Add(beatmapImportTask.ContinueWith(_ => collections.ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
if (content.HasFlagFast(StableContent.Scores))
importTasks.Add(beatmapImportTask.ContinueWith(_ => scores.ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false);
}
private async Task<StableStorage> getStableStorage()
{
if (cachedStorage != null)
return cachedStorage;
var stableStorage = game.GetStorageForStableInstall();
if (stableStorage != null)
return cachedStorage = stableStorage;
var taskCompletionSource = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
Schedule(() => dialogOverlay.Push(new StableDirectoryLocationDialog(taskCompletionSource)));
var stablePath = await taskCompletionSource.Task.ConfigureAwait(false);
return cachedStorage = new StableStorage(stablePath, desktopGameHost);
}
}
[Flags]
public enum StableContent
{
Beatmaps = 1 << 0,
Scores = 1 << 1,
Skins = 1 << 2,
Collections = 1 << 3,
All = Beatmaps | Scores | Skins | Collections
}
}

View File

@ -6,11 +6,13 @@ using Markdig.Extensions.AutoIdentifiers;
using Markdig.Extensions.Tables; using Markdig.Extensions.Tables;
using Markdig.Extensions.Yaml; using Markdig.Extensions.Yaml;
using Markdig.Syntax; using Markdig.Syntax;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown; using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
namespace osu.Game.Graphics.Containers.Markdown namespace osu.Game.Graphics.Containers.Markdown
{ {
@ -21,6 +23,16 @@ namespace osu.Game.Graphics.Containers.Markdown
LineSpacing = 21; LineSpacing = 21;
} }
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var api = parent.Get<IAPIProvider>();
// needs to be set before the base BDL call executes to avoid invalidating any already populated markdown content.
DocumentUrl = api.WebsiteRootUrl;
return base.CreateChildDependencies(parent);
}
protected override void AddMarkdownComponent(IMarkdownObject markdownObject, FillFlowContainer container, int level) protected override void AddMarkdownComponent(IMarkdownObject markdownObject, FillFlowContainer container, int level)
{ {
switch (markdownObject) switch (markdownObject)

View File

@ -1,48 +1,63 @@
// 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.Collections.Generic;
using Markdig.Syntax.Inlines; using Markdig.Syntax.Inlines;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers.Markdown; using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Sprites; using osu.Game.Online.Chat;
using osu.Framework.Input.Events;
using osu.Game.Overlays; using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers.Markdown namespace osu.Game.Graphics.Containers.Markdown
{ {
public class OsuMarkdownLinkText : MarkdownLinkText public class OsuMarkdownLinkText : MarkdownLinkText
{ {
[Resolved] [Resolved(canBeNull: true)]
private OverlayColourProvider colourProvider { get; set; } private OsuGame game { get; set; }
private SpriteText spriteText; private readonly string text;
private readonly string title;
public OsuMarkdownLinkText(string text, LinkInline linkInline) public OsuMarkdownLinkText(string text, LinkInline linkInline)
: base(text, linkInline) : base(text, linkInline)
{ {
this.text = text;
title = linkInline.Title;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
spriteText.Colour = colourProvider.Light2; var textDrawable = CreateSpriteText().With(t => t.Text = text);
InternalChildren = new Drawable[]
{
textDrawable,
new OsuMarkdownLinkCompiler(new[] { textDrawable })
{
RelativeSizeAxes = Axes.Both,
Action = OnLinkPressed,
TooltipText = title ?? Url,
}
};
} }
public override SpriteText CreateSpriteText() protected override void OnLinkPressed() => game?.HandleLink(Url);
private class OsuMarkdownLinkCompiler : DrawableLinkCompiler
{
public OsuMarkdownLinkCompiler(IEnumerable<Drawable> parts)
: base(parts)
{ {
return spriteText = base.CreateSpriteText();
} }
protected override bool OnHover(HoverEvent e) [BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{ {
spriteText.Colour = colourProvider.Light1; IdleColour = colourProvider.Light2;
return base.OnHover(e); HoverColour = colourProvider.Light1;
} }
protected override void OnHoverLost(HoverLostEvent e)
{
spriteText.Colour = colourProvider.Light2;
base.OnHoverLost(e);
} }
} }
} }

View File

@ -15,6 +15,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
@ -180,9 +181,9 @@ namespace osu.Game.Graphics.UserInterface
} }
} }
private string text; private LocalisableString text;
public string Text public LocalisableString Text
{ {
get => text; get => text;
set set

View File

@ -9,28 +9,17 @@ namespace osu.Game.Graphics.UserInterface
/// <summary> /// <summary>
/// An <see cref="OsuMenuItem"/> with three possible states. /// An <see cref="OsuMenuItem"/> with three possible states.
/// </summary> /// </summary>
public class TernaryStateMenuItem : StatefulMenuItem<TernaryState> public abstract class TernaryStateMenuItem : StatefulMenuItem<TernaryState>
{ {
/// <summary> /// <summary>
/// Creates a new <see cref="TernaryStateMenuItem"/>. /// Creates a new <see cref="TernaryStateMenuItem"/>.
/// </summary> /// </summary>
/// <param name="text">The text to display.</param> /// <param name="text">The text to display.</param>
/// <param name="nextStateFunction">A function to inform what the next state should be when this item is clicked.</param>
/// <param name="type">The type of action which this <see cref="TernaryStateMenuItem"/> performs.</param> /// <param name="type">The type of action which this <see cref="TernaryStateMenuItem"/> performs.</param>
/// <param name="action">A delegate to be invoked when this <see cref="TernaryStateMenuItem"/> is pressed.</param> /// <param name="action">A delegate to be invoked when this <see cref="TernaryStateMenuItem"/> is pressed.</param>
public TernaryStateMenuItem(string text, MenuItemType type = MenuItemType.Standard, Action<TernaryState> action = null) protected TernaryStateMenuItem(string text, Func<TernaryState, TernaryState> nextStateFunction, MenuItemType type = MenuItemType.Standard, Action<TernaryState> action = null)
: this(text, getNextState, type, action) : base(text, nextStateFunction, type, action)
{
}
/// <summary>
/// Creates a new <see cref="TernaryStateMenuItem"/>.
/// </summary>
/// <param name="text">The text to display.</param>
/// <param name="changeStateFunc">A function that mutates a state to another state after this <see cref="TernaryStateMenuItem"/> is pressed.</param>
/// <param name="type">The type of action which this <see cref="TernaryStateMenuItem"/> performs.</param>
/// <param name="action">A delegate to be invoked when this <see cref="TernaryStateMenuItem"/> is pressed.</param>
protected TernaryStateMenuItem(string text, Func<TernaryState, TernaryState> changeStateFunc, MenuItemType type, Action<TernaryState> action)
: base(text, changeStateFunc, type, action)
{ {
} }
@ -47,23 +36,5 @@ namespace osu.Game.Graphics.UserInterface
return null; return null;
} }
private static TernaryState getNextState(TernaryState state)
{
switch (state)
{
case TernaryState.False:
return TernaryState.True;
case TernaryState.Indeterminate:
return TernaryState.True;
case TernaryState.True:
return TernaryState.False;
default:
throw new ArgumentOutOfRangeException(nameof(state), state, null);
}
}
} }
} }

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 System;
namespace osu.Game.Graphics.UserInterface
{
/// <summary>
/// A ternary state menu item which will always set the item to <c>true</c> on click, even if already <c>true</c>.
/// </summary>
public class TernaryStateRadioMenuItem : TernaryStateMenuItem
{
/// <summary>
/// Creates a new <see cref="TernaryStateMenuItem"/>.
/// </summary>
/// <param name="text">The text to display.</param>
/// <param name="type">The type of action which this <see cref="TernaryStateMenuItem"/> performs.</param>
/// <param name="action">A delegate to be invoked when this <see cref="TernaryStateMenuItem"/> is pressed.</param>
public TernaryStateRadioMenuItem(string text, MenuItemType type = MenuItemType.Standard, Action<TernaryState> action = null)
: base(text, getNextState, type, action)
{
}
private static TernaryState getNextState(TernaryState state) => TernaryState.True;
}
}

View File

@ -0,0 +1,42 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
namespace osu.Game.Graphics.UserInterface
{
/// <summary>
/// A ternary state menu item which toggles the state of this item <c>false</c> if clicked when <c>true</c>.
/// </summary>
public class TernaryStateToggleMenuItem : TernaryStateMenuItem
{
/// <summary>
/// Creates a new <see cref="TernaryStateToggleMenuItem"/>.
/// </summary>
/// <param name="text">The text to display.</param>
/// <param name="type">The type of action which this <see cref="TernaryStateMenuItem"/> performs.</param>
/// <param name="action">A delegate to be invoked when this <see cref="TernaryStateMenuItem"/> is pressed.</param>
public TernaryStateToggleMenuItem(string text, MenuItemType type = MenuItemType.Standard, Action<TernaryState> action = null)
: base(text, getNextState, type, action)
{
}
private static TernaryState getNextState(TernaryState state)
{
switch (state)
{
case TernaryState.False:
return TernaryState.True;
case TernaryState.Indeterminate:
return TernaryState.True;
case TernaryState.True:
return TernaryState.False;
default:
throw new ArgumentOutOfRangeException(nameof(state), state, null);
}
}
}
}

View File

@ -73,10 +73,11 @@ namespace osu.Game.Input.Bindings
else else
{ {
KeyBindings = store.Query(ruleset?.ID, variant) KeyBindings = store.Query(ruleset?.ID, variant)
.OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.IntAction))
// this ordering is important to ensure that we read entries from the database in the order // this ordering is important to ensure that we read entries from the database in the order
// enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise // enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise
// have been eaten by the music controller due to query order. // have been eaten by the music controller due to query order.
.OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.IntAction)).ToList(); .ToList();
} }
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Platform; using osu.Framework.Platform;
@ -16,6 +17,17 @@ namespace osu.Game.Input
{ {
public event Action KeyBindingChanged; public event Action KeyBindingChanged;
/// <summary>
/// Keys which should not be allowed for gameplay input purposes.
/// </summary>
private static readonly IEnumerable<InputKey> banned_keys = new[]
{
InputKey.MouseWheelDown,
InputKey.MouseWheelLeft,
InputKey.MouseWheelUp,
InputKey.MouseWheelRight
};
public KeyBindingStore(DatabaseContextFactory contextFactory, RulesetStore rulesets, Storage storage = null) public KeyBindingStore(DatabaseContextFactory contextFactory, RulesetStore rulesets, Storage storage = null)
: base(contextFactory, storage) : base(contextFactory, storage)
{ {
@ -93,6 +105,9 @@ namespace osu.Game.Input
using (ContextFactory.GetForWrite()) using (ContextFactory.GetForWrite())
{ {
var dbKeyBinding = (DatabasedKeyBinding)keyBinding; var dbKeyBinding = (DatabasedKeyBinding)keyBinding;
Debug.Assert(dbKeyBinding.RulesetID == null || CheckValidForGameplay(keyBinding.KeyCombination));
Refresh(ref dbKeyBinding); Refresh(ref dbKeyBinding);
if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination)) if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination))
@ -103,5 +118,16 @@ namespace osu.Game.Input
KeyBindingChanged?.Invoke(); KeyBindingChanged?.Invoke();
} }
public static bool CheckValidForGameplay(KeyCombination combination)
{
foreach (var key in banned_keys)
{
if (combination.Keys.Contains(key))
return false;
}
return true;
}
} }
} }

View File

@ -0,0 +1,38 @@
// 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.Input;
using osuTK.Input;
namespace osu.Game.Input
{
public class OsuUserInputManager : UserInputManager
{
internal OsuUserInputManager()
{
}
protected override MouseButtonEventManager CreateButtonEventManagerFor(MouseButton button)
{
switch (button)
{
case MouseButton.Right:
return new RightMouseManager(button);
}
return base.CreateButtonEventManagerFor(button);
}
private class RightMouseManager : MouseButtonEventManager
{
public RightMouseManager(MouseButton button)
: base(button)
{
}
public override bool EnableDrag => true; // allow right-mouse dragging for absolute scroll in scroll containers.
public override bool EnableClick => false;
public override bool ChangeFocusOnClick => false;
}
}
}

View File

@ -0,0 +1,38 @@
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="solo" xml:space="preserve">
<value>ソロ</value>
</data>
<data name="playlists" xml:space="preserve">
<value>プレイリスト</value>
</data>
<data name="play" xml:space="preserve">
<value>遊ぶ</value>
</data>
<data name="multi" xml:space="preserve">
<value>マルチ</value>
</data>
<data name="edit" xml:space="preserve">
<value>エディット</value>
</data>
<data name="browse" xml:space="preserve">
<value>ブラウズ</value>
</data>
<data name="exit" xml:space="preserve">
<value>閉じる</value>
</data>
<data name="settings" xml:space="preserve">
<value>設定</value>
</data>
</root>

Some files were not shown because too many files have changed in this diff Show More