1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-15 16:03:01 +08:00

Merge branch 'master' into fix-select-new-from-sub-screen

This commit is contained in:
smoogipoo 2020-04-21 14:36:53 +09:00
commit fcfb2b9026
58 changed files with 1293 additions and 340 deletions

View File

@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Mania.Tests
private const double time_after_tail = 5250; private const double time_after_tail = 5250;
private List<JudgementResult> judgementResults; private List<JudgementResult> judgementResults;
private bool allJudgedFired;
/// <summary> /// <summary>
/// -----[ ]----- /// -----[ ]-----
@ -283,20 +282,15 @@ namespace osu.Game.Rulesets.Mania.Tests
{ {
if (currentPlayer == p) judgementResults.Add(result); if (currentPlayer == p) judgementResults.Add(result);
}; };
p.ScoreProcessor.AllJudged += () =>
{
if (currentPlayer == p) allJudgedFired = true;
};
}; };
LoadScreen(currentPlayer = p); LoadScreen(currentPlayer = p);
allJudgedFired = false;
judgementResults = new List<JudgementResult>(); judgementResults = new List<JudgementResult>();
}); });
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 all judged", () => allJudgedFired); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
} }
private class ScoreAccessibleReplayPlayer : ReplayPlayer private class ScoreAccessibleReplayPlayer : ReplayPlayer

View File

@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
{ {
TargetColumns = (int)Math.Max(1, roundedCircleSize); TargetColumns = (int)Math.Max(1, roundedCircleSize);
if (TargetColumns >= 10) if (TargetColumns > ManiaRuleset.MAX_STAGE_KEYS)
{ {
TargetColumns /= 2; TargetColumns /= 2;
Dual = true; Dual = true;

View File

@ -0,0 +1,64 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Input.Bindings;
namespace osu.Game.Rulesets.Mania
{
public class DualStageVariantGenerator
{
private readonly int singleStageVariant;
private readonly InputKey[] stage1LeftKeys;
private readonly InputKey[] stage1RightKeys;
private readonly InputKey[] stage2LeftKeys;
private readonly InputKey[] stage2RightKeys;
public DualStageVariantGenerator(int singleStageVariant)
{
this.singleStageVariant = singleStageVariant;
// 10K is special because it expands towards the centre of the keyboard (VM/BN), rather than towards the edges of the keyboard.
if (singleStageVariant == 10)
{
stage1LeftKeys = new[] { InputKey.Q, InputKey.W, InputKey.E, InputKey.R, InputKey.V };
stage1RightKeys = new[] { InputKey.M, InputKey.I, InputKey.O, InputKey.P, InputKey.BracketLeft };
stage2LeftKeys = new[] { InputKey.S, InputKey.D, InputKey.F, InputKey.G, InputKey.B };
stage2RightKeys = new[] { InputKey.N, InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon };
}
else
{
stage1LeftKeys = new[] { InputKey.Q, InputKey.W, InputKey.E, InputKey.R };
stage1RightKeys = new[] { InputKey.I, InputKey.O, InputKey.P, InputKey.BracketLeft };
stage2LeftKeys = new[] { InputKey.S, InputKey.D, InputKey.F, InputKey.G };
stage2RightKeys = new[] { InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon };
}
}
public IEnumerable<KeyBinding> GenerateMappings()
{
var stage1Bindings = new VariantMappingGenerator
{
LeftKeys = stage1LeftKeys,
RightKeys = stage1RightKeys,
SpecialKey = InputKey.V,
SpecialAction = ManiaAction.Special1,
NormalActionStart = ManiaAction.Key1
}.GenerateKeyBindingsFor(singleStageVariant, out var nextNormal);
var stage2Bindings = new VariantMappingGenerator
{
LeftKeys = stage2LeftKeys,
RightKeys = stage2RightKeys,
SpecialKey = InputKey.B,
SpecialAction = ManiaAction.Special2,
NormalActionStart = nextNormal
}.GenerateKeyBindingsFor(singleStageVariant, out _);
return stage1Bindings.Concat(stage2Bindings);
}
}
}

View File

@ -78,5 +78,11 @@ namespace osu.Game.Rulesets.Mania
[Description("Key 18")] [Description("Key 18")]
Key18, Key18,
[Description("Key 19")]
Key19,
[Description("Key 20")]
Key20,
} }
} }

View File

@ -35,6 +35,11 @@ namespace osu.Game.Rulesets.Mania
{ {
public class ManiaRuleset : Ruleset, ILegacyRuleset public class ManiaRuleset : Ruleset, ILegacyRuleset
{ {
/// <summary>
/// The maximum number of supported keys in a single stage.
/// </summary>
public const int MAX_STAGE_KEYS = 10;
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableManiaRuleset(this, beatmap, mods); public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableManiaRuleset(this, beatmap, mods);
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(); public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
@ -202,6 +207,7 @@ namespace osu.Game.Rulesets.Mania
new ManiaModKey7(), new ManiaModKey7(),
new ManiaModKey8(), new ManiaModKey8(),
new ManiaModKey9(), new ManiaModKey9(),
new ManiaModKey10(),
new ManiaModKey1(), new ManiaModKey1(),
new ManiaModKey2(), new ManiaModKey2(),
new ManiaModKey3()), new ManiaModKey3()),
@ -250,9 +256,9 @@ namespace osu.Game.Rulesets.Mania
{ {
get get
{ {
for (int i = 1; i <= 9; i++) for (int i = 1; i <= MAX_STAGE_KEYS; i++)
yield return (int)PlayfieldType.Single + i; yield return (int)PlayfieldType.Single + i;
for (int i = 2; i <= 18; i += 2) for (int i = 2; i <= MAX_STAGE_KEYS * 2; i += 2)
yield return (int)PlayfieldType.Dual + i; yield return (int)PlayfieldType.Dual + i;
} }
} }
@ -262,73 +268,10 @@ namespace osu.Game.Rulesets.Mania
switch (getPlayfieldType(variant)) switch (getPlayfieldType(variant))
{ {
case PlayfieldType.Single: case PlayfieldType.Single:
return new VariantMappingGenerator return new SingleStageVariantGenerator(variant).GenerateMappings();
{
LeftKeys = new[]
{
InputKey.A,
InputKey.S,
InputKey.D,
InputKey.F
},
RightKeys = new[]
{
InputKey.J,
InputKey.K,
InputKey.L,
InputKey.Semicolon
},
SpecialKey = InputKey.Space,
SpecialAction = ManiaAction.Special1,
NormalActionStart = ManiaAction.Key1,
}.GenerateKeyBindingsFor(variant, out _);
case PlayfieldType.Dual: case PlayfieldType.Dual:
int keys = getDualStageKeyCount(variant); return new DualStageVariantGenerator(getDualStageKeyCount(variant)).GenerateMappings();
var stage1Bindings = new VariantMappingGenerator
{
LeftKeys = new[]
{
InputKey.Q,
InputKey.W,
InputKey.E,
InputKey.R,
},
RightKeys = new[]
{
InputKey.X,
InputKey.C,
InputKey.V,
InputKey.B
},
SpecialKey = InputKey.S,
SpecialAction = ManiaAction.Special1,
NormalActionStart = ManiaAction.Key1
}.GenerateKeyBindingsFor(keys, out var nextNormal);
var stage2Bindings = new VariantMappingGenerator
{
LeftKeys = new[]
{
InputKey.Number7,
InputKey.Number8,
InputKey.Number9,
InputKey.Number0
},
RightKeys = new[]
{
InputKey.K,
InputKey.L,
InputKey.Semicolon,
InputKey.Quote
},
SpecialKey = InputKey.I,
SpecialAction = ManiaAction.Special2,
NormalActionStart = nextNormal
}.GenerateKeyBindingsFor(keys, out _);
return stage1Bindings.Concat(stage2Bindings);
} }
return Array.Empty<KeyBinding>(); return Array.Empty<KeyBinding>();
@ -364,59 +307,6 @@ namespace osu.Game.Rulesets.Mania
{ {
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast<int>().OrderByDescending(i => i).First(v => variant >= v); return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast<int>().OrderByDescending(i => i).First(v => variant >= v);
} }
private class VariantMappingGenerator
{
/// <summary>
/// All the <see cref="InputKey"/>s available to the left hand.
/// </summary>
public InputKey[] LeftKeys;
/// <summary>
/// All the <see cref="InputKey"/>s available to the right hand.
/// </summary>
public InputKey[] RightKeys;
/// <summary>
/// The <see cref="InputKey"/> for the special key.
/// </summary>
public InputKey SpecialKey;
/// <summary>
/// The <see cref="ManiaAction"/> at which the normal columns should begin.
/// </summary>
public ManiaAction NormalActionStart;
/// <summary>
/// The <see cref="ManiaAction"/> for the special column.
/// </summary>
public ManiaAction SpecialAction;
/// <summary>
/// Generates a list of <see cref="KeyBinding"/>s for a specific number of columns.
/// </summary>
/// <param name="columns">The number of columns that need to be bound.</param>
/// <param name="nextNormalAction">The next <see cref="ManiaAction"/> to use for normal columns.</param>
/// <returns>The keybindings.</returns>
public IEnumerable<KeyBinding> GenerateKeyBindingsFor(int columns, out ManiaAction nextNormalAction)
{
ManiaAction currentNormalAction = NormalActionStart;
var bindings = new List<KeyBinding>();
for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++)
bindings.Add(new KeyBinding(LeftKeys[i], currentNormalAction++));
if (columns % 2 == 1)
bindings.Add(new KeyBinding(SpecialKey, SpecialAction));
for (int i = 0; i < columns / 2; i++)
bindings.Add(new KeyBinding(RightKeys[i], currentNormalAction++));
nextNormalAction = currentNormalAction;
return bindings;
}
}
} }
public enum PlayfieldType public enum PlayfieldType

View File

@ -0,0 +1,13 @@
// 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.Mods
{
public class ManiaModKey10 : ManiaKeyMod
{
public override int KeyCount => 10;
public override string Name => "Ten Keys";
public override string Acronym => "10K";
public override string Description => @"Play with ten keys.";
}
}

View File

@ -0,0 +1,41 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Input.Bindings;
namespace osu.Game.Rulesets.Mania
{
public class SingleStageVariantGenerator
{
private readonly int variant;
private readonly InputKey[] leftKeys;
private readonly InputKey[] rightKeys;
public SingleStageVariantGenerator(int variant)
{
this.variant = variant;
// 10K is special because it expands towards the centre of the keyboard (V/N), rather than towards the edges of the keyboard.
if (variant == 10)
{
leftKeys = new[] { InputKey.A, InputKey.S, InputKey.D, InputKey.F, InputKey.V };
rightKeys = new[] { InputKey.N, InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon };
}
else
{
leftKeys = new[] { InputKey.A, InputKey.S, InputKey.D, InputKey.F };
rightKeys = new[] { InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon };
}
}
public IEnumerable<KeyBinding> GenerateMappings() => new VariantMappingGenerator
{
LeftKeys = leftKeys,
RightKeys = rightKeys,
SpecialKey = InputKey.Space,
SpecialAction = ManiaAction.Special1,
NormalActionStart = ManiaAction.Key1,
}.GenerateKeyBindingsFor(variant, out _);
}
}

View File

@ -0,0 +1,61 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Input.Bindings;
namespace osu.Game.Rulesets.Mania
{
public class VariantMappingGenerator
{
/// <summary>
/// All the <see cref="InputKey"/>s available to the left hand.
/// </summary>
public InputKey[] LeftKeys;
/// <summary>
/// All the <see cref="InputKey"/>s available to the right hand.
/// </summary>
public InputKey[] RightKeys;
/// <summary>
/// The <see cref="InputKey"/> for the special key.
/// </summary>
public InputKey SpecialKey;
/// <summary>
/// The <see cref="ManiaAction"/> at which the normal columns should begin.
/// </summary>
public ManiaAction NormalActionStart;
/// <summary>
/// The <see cref="ManiaAction"/> for the special column.
/// </summary>
public ManiaAction SpecialAction;
/// <summary>
/// Generates a list of <see cref="KeyBinding"/>s for a specific number of columns.
/// </summary>
/// <param name="columns">The number of columns that need to be bound.</param>
/// <param name="nextNormalAction">The next <see cref="ManiaAction"/> to use for normal columns.</param>
/// <returns>The keybindings.</returns>
public IEnumerable<KeyBinding> GenerateKeyBindingsFor(int columns, out ManiaAction nextNormalAction)
{
ManiaAction currentNormalAction = NormalActionStart;
var bindings = new List<KeyBinding>();
for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++)
bindings.Add(new KeyBinding(LeftKeys[i], currentNormalAction++));
if (columns % 2 == 1)
bindings.Add(new KeyBinding(SpecialKey, SpecialAction));
for (int i = 0; i < columns / 2; i++)
bindings.Add(new KeyBinding(RightKeys[i], currentNormalAction++));
nextNormalAction = currentNormalAction;
return bindings;
}
}
}

View File

@ -0,0 +1,106 @@
// 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.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModHidden : ModTestScene
{
public TestSceneOsuModHidden()
: base(new OsuRuleset())
{
}
[Test]
public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData
{
Mod = new OsuModHidden(),
Autoplay = true,
PassCondition = checkSomeHit
});
[Test]
public void FirstCircleAfterTwoSpinners() => CreateModTest(new ModTestData
{
Mod = new OsuModHidden(),
Autoplay = true,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Spinner
{
Position = new Vector2(256, 192),
EndTime = 1000,
},
new Spinner
{
Position = new Vector2(256, 192),
StartTime = 1200,
EndTime = 2200,
},
new HitCircle
{
Position = new Vector2(300, 192),
StartTime = 3200,
},
new HitCircle
{
Position = new Vector2(384, 192),
StartTime = 4200,
}
}
},
PassCondition = checkSomeHit
});
[Test]
public void FirstSliderAfterTwoSpinners() => CreateModTest(new ModTestData
{
Mod = new OsuModHidden(),
Autoplay = true,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Spinner
{
Position = new Vector2(256, 192),
EndTime = 1000,
},
new Spinner
{
Position = new Vector2(256, 192),
StartTime = 1200,
EndTime = 2200,
},
new Slider
{
StartTime = 3200,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
},
new Slider
{
StartTime = 5200,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
}
}
},
PassCondition = checkSomeHit
});
private bool checkSomeHit()
{
return Player.ScoreProcessor.JudgedHits >= 4;
}
}
}

View File

@ -354,7 +354,6 @@ namespace osu.Game.Rulesets.Osu.Tests
private ScoreAccessibleReplayPlayer currentPlayer; private ScoreAccessibleReplayPlayer currentPlayer;
private List<JudgementResult> judgementResults; private List<JudgementResult> judgementResults;
private bool allJudgedFired;
private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames) private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames)
{ {
@ -380,20 +379,15 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
if (currentPlayer == p) judgementResults.Add(result); if (currentPlayer == p) judgementResults.Add(result);
}; };
p.ScoreProcessor.AllJudged += () =>
{
if (currentPlayer == p) allJudgedFired = true;
};
}; };
LoadScreen(currentPlayer = p); LoadScreen(currentPlayer = p);
allJudgedFired = false;
judgementResults = new List<JudgementResult>(); judgementResults = new List<JudgementResult>();
}); });
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 all judged", () => allJudgedFired); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
} }
private class TestHitCircle : HitCircle private class TestHitCircle : HitCircle

View File

@ -47,7 +47,6 @@ namespace osu.Game.Rulesets.Osu.Tests
private const double time_slider_end = 4000; private const double time_slider_end = 4000;
private List<JudgementResult> judgementResults; private List<JudgementResult> judgementResults;
private bool allJudgedFired;
/// <summary> /// <summary>
/// Scenario: /// Scenario:
@ -375,20 +374,15 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
if (currentPlayer == p) judgementResults.Add(result); if (currentPlayer == p) judgementResults.Add(result);
}; };
p.ScoreProcessor.AllJudged += () =>
{
if (currentPlayer == p) allJudgedFired = true;
};
}; };
LoadScreen(currentPlayer = p); LoadScreen(currentPlayer = p);
allJudgedFired = false;
judgementResults = new List<JudgementResult>(); judgementResults = new List<JudgementResult>();
}); });
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 all judged", () => allJudgedFired); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
} }
private class ScoreAccessibleReplayPlayer : ReplayPlayer private class ScoreAccessibleReplayPlayer : ReplayPlayer

View File

@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Mods
private const double fade_in_duration_multiplier = 0.4; private const double fade_in_duration_multiplier = 0.4;
private const double fade_out_duration_multiplier = 0.3; private const double fade_out_duration_multiplier = 0.3;
protected override bool IsFirstHideableObject(DrawableHitObject hitObject) => !(hitObject is DrawableSpinner);
public override void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables) public override void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
{ {
static void adjustFadeIn(OsuHitObject h) => h.TimeFadeIn = h.TimePreempt * fade_in_duration_multiplier; static void adjustFadeIn(OsuHitObject h) => h.TimeFadeIn = h.TimePreempt * fade_in_duration_multiplier;

View File

@ -6,7 +6,7 @@ using System.Collections.Generic;
using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Rulesets.Taiko.Skinning;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{ {
public abstract class TaikoSkinnableTestScene : SkinnableTestScene public abstract class TaikoSkinnableTestScene : SkinnableTestScene
{ {

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;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Skinning;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
[TestFixture]
public class TestSceneDrawableDrumRoll : TaikoSkinnableTestScene
{
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[]
{
typeof(DrawableDrumRoll),
typeof(DrawableDrumRollTick),
typeof(LegacyDrumRoll),
}).ToList();
[Cached(typeof(IScrollingInfo))]
private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo
{
Direction = { Value = ScrollingDirection.Left },
TimeRange = { Value = 5000 },
};
[BackgroundDependencyLoader]
private void load()
{
AddStep("Drum roll", () => SetContents(() =>
{
var hoc = new ScrollingHitObjectContainer();
hoc.Add(new DrawableDrumRoll(createDrumRollAtCurrentTime())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
});
return hoc;
}));
AddStep("Drum roll (strong)", () => SetContents(() =>
{
var hoc = new ScrollingHitObjectContainer();
hoc.Add(new DrawableDrumRoll(createDrumRollAtCurrentTime(true))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
});
return hoc;
}));
}
private DrumRoll createDrumRollAtCurrentTime(bool strong = false)
{
var drumroll = new DrumRoll
{
IsStrong = strong,
StartTime = Time.Current + 1000,
Duration = 4000,
};
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint { BeatLength = 500 });
drumroll.ApplyDefaults(cpi, new BeatmapDifficulty());
return drumroll;
}
}
}

View File

@ -13,7 +13,7 @@ using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Rulesets.Taiko.Skinning;
namespace osu.Game.Rulesets.Taiko.Tests namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{ {
[TestFixture] [TestFixture]
public class TestSceneDrawableHit : TaikoSkinnableTestScene public class TestSceneDrawableHit : TaikoSkinnableTestScene
@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
typeof(DrawableCentreHit), typeof(DrawableCentreHit),
typeof(DrawableRimHit), typeof(DrawableRimHit),
typeof(LegacyHit), typeof(LegacyHit),
typeof(LegacyCirclePiece),
}).ToList(); }).ToList();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]

View File

@ -6,14 +6,14 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osuTK;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Rulesets.Taiko.Skinning;
using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.Taiko.UI;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Tests namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{ {
[TestFixture] [TestFixture]
public class TestSceneInputDrum : TaikoSkinnableTestScene public class TestSceneInputDrum : TaikoSkinnableTestScene

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
[Test] [Test]
public void TestZeroTickTimeOffsets() public void TestZeroTickTimeOffsets()
{ {
AddUntilStep("gameplay finished", () => Player.ScoreProcessor.HasCompleted); AddUntilStep("gameplay finished", () => Player.ScoreProcessor.HasCompleted.Value);
AddAssert("all tick offsets are 0", () => Player.Results.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0)); AddAssert("all tick offsets are 0", () => Player.Results.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0));
} }

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 osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -16,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{ {
} }
protected override CompositeDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit), protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit),
_ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit); _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
} }
} }

View File

@ -14,6 +14,8 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{ {
@ -29,25 +31,29 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
/// </summary> /// </summary>
private int rollingHits; private int rollingHits;
private readonly Container<DrawableDrumRollTick> tickContainer; private Container tickContainer;
private Color4 colourIdle; private Color4 colourIdle;
private Color4 colourEngaged; private Color4 colourEngaged;
private ElongatedCirclePiece elongatedPiece;
public DrawableDrumRoll(DrumRoll drumRoll) public DrawableDrumRoll(DrumRoll drumRoll)
: base(drumRoll) : base(drumRoll)
{ {
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
elongatedPiece.Add(tickContainer = new Container<DrawableDrumRollTick> { RelativeSizeAxes = Axes.Both });
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
elongatedPiece.AccentColour = colourIdle = colours.YellowDark; colourIdle = colours.YellowDark;
colourEngaged = colours.YellowDarker; colourEngaged = colours.YellowDarker;
updateColour();
Content.Add(tickContainer = new Container { RelativeSizeAxes = Axes.Both });
if (MainPiece.Drawable is IHasAccentColour accentMain)
accentMain.AccentColour = colourIdle;
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -86,7 +92,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
return base.CreateNestedHitObject(hitObject); return base.CreateNestedHitObject(hitObject);
} }
protected override CompositeDrawable CreateMainPiece() => elongatedPiece = new ElongatedCirclePiece(); protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollBody),
_ => new ElongatedCirclePiece());
public override bool OnPressed(TaikoAction action) => false; public override bool OnPressed(TaikoAction action) => false;
@ -102,8 +109,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
rollingHits = Math.Clamp(rollingHits, 0, rolling_hits_for_engaged_colour); rollingHits = Math.Clamp(rollingHits, 0, rolling_hits_for_engaged_colour);
Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1); updateColour();
(MainPiece as IHasAccentColour)?.FadeAccent(newColour, 100);
} }
protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void CheckForResult(bool userTriggered, double timeOffset)
@ -132,8 +138,22 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
} }
} }
protected override void Update()
{
base.Update();
OriginPosition = new Vector2(DrawHeight);
Content.X = DrawHeight / 2;
}
protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this); protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this);
private void updateColour()
{
Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1);
(MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, 100);
}
private class StrongNestedHit : DrawableStrongNestedHit private class StrongNestedHit : DrawableStrongNestedHit
{ {
public StrongNestedHit(StrongHitObject strong, DrawableDrumRoll drumRoll) public StrongNestedHit(StrongHitObject strong, DrawableDrumRoll drumRoll)

View File

@ -3,10 +3,10 @@
using System; using System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{ {
@ -20,10 +20,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
public override bool DisplayResult => false; public override bool DisplayResult => false;
protected override CompositeDrawable CreateMainPiece() => new TickPiece protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollTick),
_ => new TickPiece
{ {
Filled = HitObject.FirstTick Filled = HitObject.FirstTick
}; });
protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void CheckForResult(bool userTriggered, double timeOffset)
{ {

View File

@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
// If we're far enough away from the left stage, we should bring outselves in front of it // If we're far enough away from the left stage, we should bring outselves in front of it
ProxyContent(); ProxyContent();
var flash = (MainPiece as CirclePiece)?.FlashBox; var flash = (MainPiece.Drawable as CirclePiece)?.FlashBox;
flash?.FadeTo(0.9f).FadeOut(300); flash?.FadeTo(0.9f).FadeOut(300);
const float gravity_time = 300; const float gravity_time = 300;

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 osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -16,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{ {
} }
protected override CompositeDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit), protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit),
_ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit); _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
} }
} }

View File

@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{ {
@ -114,12 +115,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); targetRing.BorderColour = colours.YellowDark.Opacity(0.25f);
} }
protected override CompositeDrawable CreateMainPiece() => new SwellCirclePiece protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Swell),
_ => new SwellCirclePiece
{ {
// to allow for rotation transform // to allow for rotation transform
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}; });
protected override void LoadComplete() protected override void LoadComplete()
{ {
@ -184,7 +186,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
.Then() .Then()
.FadeTo(completion / 8, 2000, Easing.OutQuint); .FadeTo(completion / 8, 2000, Easing.OutQuint);
MainPiece.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint); MainPiece.Drawable.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint);
expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint);

View File

@ -2,9 +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 osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{ {
@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
public override bool OnPressed(TaikoAction action) => false; public override bool OnPressed(TaikoAction action) => false;
protected override CompositeDrawable CreateMainPiece() => new TickPiece(); protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollTick),
_ => new TickPiece());
} }
} }

View File

@ -11,6 +11,7 @@ using System.Collections.Generic;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{ {
@ -115,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
public new TObject HitObject; public new TObject HitObject;
protected readonly Vector2 BaseSize; protected readonly Vector2 BaseSize;
protected readonly CompositeDrawable MainPiece; protected readonly SkinnableDrawable MainPiece;
private readonly Container<DrawableStrongNestedHit> strongHitContainer; private readonly Container<DrawableStrongNestedHit> strongHitContainer;
@ -167,7 +168,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
// Normal and clap samples are handled by the drum // Normal and clap samples are handled by the drum
protected override IEnumerable<HitSampleInfo> GetSamples() => HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP); protected override IEnumerable<HitSampleInfo> GetSamples() => HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP);
protected abstract CompositeDrawable CreateMainPiece(); protected abstract SkinnableDrawable CreateMainPiece();
/// <summary> /// <summary>
/// Creates the handler for this <see cref="DrawableHitObject"/>'s <see cref="StrongHitObject"/>. /// Creates the handler for this <see cref="DrawableHitObject"/>'s <see cref="StrongHitObject"/>.

View File

@ -10,6 +10,7 @@ using osuTK.Graphics;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
/// for a usage example. /// for a usage example.
/// </para> /// </para>
/// </summary> /// </summary>
public abstract class CirclePiece : BeatSyncedContainer public abstract class CirclePiece : BeatSyncedContainer, IHasAccentColour
{ {
public const float SYMBOL_SIZE = 0.45f; public const float SYMBOL_SIZE = 0.45f;
public const float SYMBOL_BORDER = 8; public const float SYMBOL_BORDER = 8;

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 osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
{ {
@ -12,18 +14,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
} }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AccentColour = colours.YellowDark;
}
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
var padding = Content.DrawHeight * Content.Width / 2;
Content.Padding = new MarginPadding
{
Left = padding,
Right = padding,
};
Width = Parent.DrawSize.X + DrawHeight; Width = Parent.DrawSize.X + DrawHeight;
} }
} }

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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.Skinning
{
public class LegacyCirclePiece : CompositeDrawable, IHasAccentColour
{
private Drawable backgroundLayer;
public LegacyCirclePiece()
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin, DrawableHitObject drawableHitObject)
{
Drawable getDrawableFor(string lookup)
{
const string normal_hit = "taikohit";
const string big_hit = "taikobig";
string prefix = ((drawableHitObject as DrawableTaikoHitObject)?.HitObject.IsStrong ?? false) ? big_hit : normal_hit;
return skin.GetAnimation($"{prefix}{lookup}", true, false) ??
// fallback to regular size if "big" version doesn't exist.
skin.GetAnimation($"{normal_hit}{lookup}", true, false);
}
// backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer.
AddInternal(backgroundLayer = getDrawableFor("circle"));
var foregroundLayer = getDrawableFor("circleoverlay");
if (foregroundLayer != null)
AddInternal(foregroundLayer);
// Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat).
// For now just stop at first frame for sanity.
foreach (var c in InternalChildren)
{
(c as IFramedAnimation)?.Stop();
c.Anchor = Anchor.Centre;
c.Origin = Anchor.Centre;
}
}
protected override void LoadComplete()
{
base.LoadComplete();
updateAccentColour();
}
protected override void Update()
{
base.Update();
// Not all skins (including the default osu-stable) have similar sizes for "hitcircle" and "hitcircleoverlay".
// This ensures they are scaled relative to each other but also match the expected DrawableHit size.
foreach (var c in InternalChildren)
c.Scale = new Vector2(DrawHeight / 128);
}
private Color4 accentColour;
public Color4 AccentColour
{
get => accentColour;
set
{
if (value == accentColour)
return;
accentColour = value;
if (IsLoaded)
updateAccentColour();
}
}
private void updateAccentColour()
{
backgroundLayer.Colour = accentColour;
}
}
}

View File

@ -0,0 +1,83 @@
// 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.Sprites;
using osu.Game.Graphics;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.Skinning
{
public class LegacyDrumRoll : CompositeDrawable, IHasAccentColour
{
private LegacyCirclePiece headCircle;
private Sprite body;
private Sprite end;
public LegacyDrumRoll()
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin, OsuColour colours)
{
InternalChildren = new Drawable[]
{
end = new Sprite
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Texture = skin.GetTexture("taiko-roll-end"),
FillMode = FillMode.Fit,
},
body = new Sprite
{
RelativeSizeAxes = Axes.Both,
Texture = skin.GetTexture("taiko-roll-middle"),
},
headCircle = new LegacyCirclePiece
{
RelativeSizeAxes = Axes.Y,
},
};
AccentColour = colours.YellowDark;
}
protected override void LoadComplete()
{
base.LoadComplete();
updateAccentColour();
}
private Color4 accentColour;
public Color4 AccentColour
{
get => accentColour;
set
{
if (value == accentColour)
return;
accentColour = value;
if (IsLoaded)
updateAccentColour();
}
}
private void updateAccentColour()
{
headCircle.AccentColour = accentColour;
body.Colour = accentColour;
end.Colour = accentColour;
}
}
}

View File

@ -2,90 +2,25 @@
// 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.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.Skinning namespace osu.Game.Rulesets.Taiko.Skinning
{ {
public class LegacyHit : CompositeDrawable, IHasAccentColour public class LegacyHit : LegacyCirclePiece
{ {
private readonly TaikoSkinComponents component; private readonly TaikoSkinComponents component;
private Drawable backgroundLayer;
public LegacyHit(TaikoSkinComponents component) public LegacyHit(TaikoSkinComponents component)
{ {
this.component = component; this.component = component;
RelativeSizeAxes = Axes.Both;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skin, DrawableHitObject drawableHitObject) private void load()
{ {
Drawable getDrawableFor(string lookup)
{
const string normal_hit = "taikohit";
const string big_hit = "taikobig";
string prefix = ((drawableHitObject as DrawableTaikoHitObject)?.HitObject.IsStrong ?? false) ? big_hit : normal_hit;
return skin.GetAnimation($"{prefix}{lookup}", true, false) ??
// fallback to regular size if "big" version doesn't exist.
skin.GetAnimation($"{normal_hit}{lookup}", true, false);
}
// backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer.
AddInternal(backgroundLayer = getDrawableFor("circle"));
var foregroundLayer = getDrawableFor("circleoverlay");
if (foregroundLayer != null)
AddInternal(foregroundLayer);
// Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat).
// For now just stop at first frame for sanity.
foreach (var c in InternalChildren)
{
(c as IFramedAnimation)?.Stop();
c.Anchor = Anchor.Centre;
c.Origin = Anchor.Centre;
}
AccentColour = component == TaikoSkinComponents.CentreHit AccentColour = component == TaikoSkinComponents.CentreHit
? new Color4(235, 69, 44, 255) ? new Color4(235, 69, 44, 255)
: new Color4(67, 142, 172, 255); : new Color4(67, 142, 172, 255);
} }
protected override void Update()
{
base.Update();
// Not all skins (including the default osu-stable) have similar sizes for "hitcircle" and "hitcircleoverlay".
// This ensures they are scaled relative to each other but also match the expected DrawableHit size.
foreach (var c in InternalChildren)
c.Scale = new Vector2(DrawWidth / 128);
}
private Color4 accentColour;
public Color4 AccentColour
{
get => accentColour;
set
{
if (value == accentColour)
return;
backgroundLayer.Colour = accentColour = value;
}
}
} }
} }

View File

@ -27,6 +27,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning
switch (taikoComponent.Component) switch (taikoComponent.Component)
{ {
case TaikoSkinComponents.DrumRollBody:
if (GetTexture("taiko-roll-middle") != null)
return new LegacyDrumRoll();
return null;
case TaikoSkinComponents.InputDrum: case TaikoSkinComponents.InputDrum:
if (GetTexture("taiko-bar-left") != null) if (GetTexture("taiko-bar-left") != null)
return new LegacyInputDrum(); return new LegacyInputDrum();
@ -40,6 +46,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning
return new LegacyHit(taikoComponent.Component); return new LegacyHit(taikoComponent.Component);
return null; return null;
case TaikoSkinComponents.DrumRollTick:
return this.GetAnimation("sliderscorepoint", false, false);
} }
return source.GetDrawableComponent(component); return source.GetDrawableComponent(component);

View File

@ -7,6 +7,9 @@ namespace osu.Game.Rulesets.Taiko
{ {
InputDrum, InputDrum,
CentreHit, CentreHit,
RimHit RimHit,
DrumRollBody,
DrumRollTick,
Swell
} }
} }

View File

@ -0,0 +1,139 @@
// 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.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Storyboards;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneCompletionCancellation : PlayerTestScene
{
private Track track;
[Resolved]
private AudioManager audio { get; set; }
private int resultsDisplayWaitCount =>
(int)((Screens.Play.Player.RESULTS_DISPLAY_DELAY / TimePerAction) * 2);
protected override bool AllowFail => false;
public TestSceneCompletionCancellation()
: base(new OsuRuleset())
{
}
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
// Ensure track has actually running before attempting to seek
AddUntilStep("wait for track to start running", () => track.IsRunning);
}
[Test]
public void TestCancelCompletionOnRewind()
{
complete();
cancel();
checkNoRanking();
}
[Test]
public void TestReCompleteAfterCancellation()
{
complete();
cancel();
complete();
AddUntilStep("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).GotoRankingInvoked);
}
/// <summary>
/// Tests whether can still pause after cancelling completion by reverting <see cref="IScreen.ValidForResume"/> back to true.
/// </summary>
[Test]
public void TestCanPauseAfterCancellation()
{
complete();
cancel();
AddStep("pause", () => Player.Pause());
AddAssert("paused successfully", () => Player.GameplayClockContainer.IsPaused.Value);
checkNoRanking();
}
private void complete()
{
AddStep("seek to completion", () => track.Seek(5000));
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
}
private void cancel()
{
AddStep("rewind to cancel", () => track.Seek(4000));
AddUntilStep("completion cleared by processor", () => !Player.ScoreProcessor.HasCompleted.Value);
}
private void checkNoRanking()
{
// wait to ensure there was no attempt of pushing the results screen.
AddWaitStep("wait", resultsDisplayWaitCount);
AddAssert("no attempt to push ranking", () => !((FakeRankingPushPlayer)Player).GotoRankingInvoked);
}
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio);
track = working.Track;
return working;
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap();
for (int i = 1; i <= 19; i++)
{
beatmap.HitObjects.Add(new HitCircle
{
Position = new Vector2(256, 192),
StartTime = i * 250,
});
}
return beatmap;
}
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new FakeRankingPushPlayer();
public class FakeRankingPushPlayer : TestPlayer
{
public bool GotoRankingInvoked;
public FakeRankingPushPlayer()
: base(true, true)
{
}
protected override void GotoRanking()
{
GotoRankingInvoked = 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 System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Overlays;
using osu.Game.Overlays.Dashboard;
using osu.Game.Overlays.Dashboard.Friends;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneDashboardOverlay : OsuTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(DashboardOverlay),
typeof(DashboardOverlayHeader),
typeof(FriendDisplay)
};
protected override bool UseOnlineAPI => true;
private readonly DashboardOverlay overlay;
public TestSceneDashboardOverlay()
{
Add(overlay = new DashboardOverlay());
}
[Test]
public void TestShow()
{
AddStep("Show", overlay.Show);
}
[Test]
public void TestHide()
{
AddStep("Hide", overlay.Hide);
}
}
}

View File

@ -10,6 +10,7 @@ using osu.Game.Users;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Online.API;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
{ {
@ -27,7 +28,7 @@ namespace osu.Game.Tests.Visual.Online
[Cached] [Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
private FriendDisplay display; private TestFriendDisplay display;
[SetUp] [SetUp]
public void Setup() => Schedule(() => public void Setup() => Schedule(() =>
@ -35,7 +36,7 @@ namespace osu.Game.Tests.Visual.Online
Child = new BasicScrollContainer Child = new BasicScrollContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = display = new FriendDisplay() Child = display = new TestFriendDisplay()
}; };
}); });
@ -83,5 +84,17 @@ namespace osu.Game.Tests.Visual.Online
LastVisit = DateTimeOffset.Now LastVisit = DateTimeOffset.Now
} }
}; };
private class TestFriendDisplay : FriendDisplay
{
public void Fetch()
{
base.APIStateChanged(API, APIState.Online);
}
public override void APIStateChanged(IAPIProvider api, APIState state)
{
}
}
} }
} }

View File

@ -359,6 +359,68 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null); AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null);
} }
[Test]
public void TestPresentNewRulesetNewBeatmap()
{
createSongSelect();
changeRuleset(2);
addRulesetImportStep(2);
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2);
addRulesetImportStep(0);
addRulesetImportStep(0);
addRulesetImportStep(0);
BeatmapInfo target = null;
AddStep("select beatmap/ruleset externally", () =>
{
target = manager.GetAllUsableBeatmapSets()
.Last(b => b.Beatmaps.Any(bi => bi.RulesetID == 0)).Beatmaps.Last();
Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == 0);
Beatmap.Value = manager.GetWorkingBeatmap(target);
});
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target));
// this is an important check, to make sure updateComponentFromBeatmap() was actually run
AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo == target);
}
[Test]
public void TestPresentNewBeatmapNewRuleset()
{
createSongSelect();
changeRuleset(2);
addRulesetImportStep(2);
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2);
addRulesetImportStep(0);
addRulesetImportStep(0);
addRulesetImportStep(0);
BeatmapInfo target = null;
AddStep("select beatmap/ruleset externally", () =>
{
target = manager.GetAllUsableBeatmapSets()
.Last(b => b.Beatmaps.Any(bi => bi.RulesetID == 0)).Beatmaps.Last();
Beatmap.Value = manager.GetWorkingBeatmap(target);
Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == 0);
});
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target));
AddUntilStep("has correct ruleset", () => Ruleset.Value.ID == 0);
// this is an important check, to make sure updateComponentFromBeatmap() was actually run
AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo == target);
}
[Test] [Test]
public void TestRulesetChangeResetsMods() public void TestRulesetChangeResetsMods()
{ {

View File

@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual
typeof(OnScreenDisplay), typeof(OnScreenDisplay),
typeof(NotificationOverlay), typeof(NotificationOverlay),
typeof(DirectOverlay), typeof(DirectOverlay),
typeof(SocialOverlay), typeof(DashboardOverlay),
typeof(ChannelManager), typeof(ChannelManager),
typeof(ChatOverlay), typeof(ChatOverlay),
typeof(SettingsOverlay), typeof(SettingsOverlay),

View File

@ -56,6 +56,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <summary> /// <summary>
/// All control points, of all types. /// All control points, of all types.
/// </summary> /// </summary>
[JsonIgnore]
public IEnumerable<ControlPoint> AllControlPoints => Groups.SelectMany(g => g.ControlPoints).ToArray(); public IEnumerable<ControlPoint> AllControlPoints => Groups.SelectMany(g => g.ControlPoints).ToArray();
/// <summary> /// <summary>

View File

@ -18,24 +18,32 @@ namespace osu.Game.Online.API
public T Result { get; private set; } public T Result { get; private set; }
protected APIRequest()
{
base.Success += () => TriggerSuccess(((OsuJsonWebRequest<T>)WebRequest)?.ResponseObject);
}
/// <summary> /// <summary>
/// Invoked on successful completion of an API request. /// Invoked on successful completion of an API request.
/// This will be scheduled to the API's internal scheduler (run on update thread automatically). /// This will be scheduled to the API's internal scheduler (run on update thread automatically).
/// </summary> /// </summary>
public new event APISuccessHandler<T> Success; public new event APISuccessHandler<T> Success;
protected override void PostProcess()
{
base.PostProcess();
Result = ((OsuJsonWebRequest<T>)WebRequest)?.ResponseObject;
}
internal void TriggerSuccess(T result) internal void TriggerSuccess(T result)
{ {
if (Result != null) if (Result != null)
throw new InvalidOperationException("Attempted to trigger success more than once"); throw new InvalidOperationException("Attempted to trigger success more than once");
Result = result; Result = result;
Success?.Invoke(result);
TriggerSuccess();
}
internal override void TriggerSuccess()
{
base.TriggerSuccess();
Success?.Invoke(Result);
} }
} }
@ -99,6 +107,8 @@ namespace osu.Game.Online.API
if (checkAndScheduleFailure()) if (checkAndScheduleFailure())
return; return;
PostProcess();
API.Schedule(delegate API.Schedule(delegate
{ {
if (cancelled) return; if (cancelled) return;
@ -107,7 +117,14 @@ namespace osu.Game.Online.API
}); });
} }
internal void TriggerSuccess() /// <summary>
/// Perform any post-processing actions after a successful request.
/// </summary>
protected virtual void PostProcess()
{
}
internal virtual void TriggerSuccess()
{ {
Success?.Invoke(); Success?.Invoke();
} }

View File

@ -67,7 +67,7 @@ namespace osu.Game
private DirectOverlay direct; private DirectOverlay direct;
private SocialOverlay social; private DashboardOverlay dashboard;
private UserProfileOverlay userProfile; private UserProfileOverlay userProfile;
@ -611,7 +611,7 @@ namespace osu.Game
//overlay elements //overlay elements
loadComponentSingleFile(direct = new DirectOverlay(), overlayContent.Add, true); loadComponentSingleFile(direct = new DirectOverlay(), overlayContent.Add, true);
loadComponentSingleFile(social = new SocialOverlay(), overlayContent.Add, true); loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true); var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true);
loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true); loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true);
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true); loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
@ -670,7 +670,7 @@ namespace osu.Game
} }
// ensure only one of these overlays are open at once. // ensure only one of these overlays are open at once.
var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, social, direct, changelogOverlay, rankingsOverlay }; var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, dashboard, direct, changelogOverlay, rankingsOverlay };
foreach (var overlay in singleDisplayOverlays) foreach (var overlay in singleDisplayOverlays)
{ {
@ -842,7 +842,7 @@ namespace osu.Game
return true; return true;
case GlobalAction.ToggleSocial: case GlobalAction.ToggleSocial:
social.ToggleVisibility(); dashboard.ToggleVisibility();
return true; return true;
case GlobalAction.ResetInputSettings: case GlobalAction.ResetInputSettings:

View File

@ -0,0 +1,24 @@
// 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.Overlays.Dashboard
{
public class DashboardOverlayHeader : TabControlOverlayHeader<DashboardOverlayTabs>
{
protected override OverlayTitle CreateTitle() => new DashboardTitle();
private class DashboardTitle : OverlayTitle
{
public DashboardTitle()
{
Title = "dashboard";
IconTexture = "Icons/changelog";
}
}
}
public enum DashboardOverlayTabs
{
Friends
}
}

View File

@ -16,7 +16,7 @@ using osuTK;
namespace osu.Game.Overlays.Dashboard.Friends namespace osu.Game.Overlays.Dashboard.Friends
{ {
public class FriendDisplay : CompositeDrawable public class FriendDisplay : OverlayView<List<User>>
{ {
private List<User> users = new List<User>(); private List<User> users = new List<User>();
@ -26,34 +26,29 @@ namespace osu.Game.Overlays.Dashboard.Friends
set set
{ {
users = value; users = value;
onlineStreamControl.Populate(value); onlineStreamControl.Populate(value);
} }
} }
[Resolved]
private IAPIProvider api { get; set; }
private GetFriendsRequest request;
private CancellationTokenSource cancellationToken; private CancellationTokenSource cancellationToken;
private Drawable currentContent; private Drawable currentContent;
private readonly FriendOnlineStreamControl onlineStreamControl; private FriendOnlineStreamControl onlineStreamControl;
private readonly Box background; private Box background;
private readonly Box controlBackground; private Box controlBackground;
private readonly UserListToolbar userListToolbar; private UserListToolbar userListToolbar;
private readonly Container itemsPlaceholder; private Container itemsPlaceholder;
private readonly LoadingLayer loading; private LoadingLayer loading;
public FriendDisplay() [BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{ {
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new FillFlowContainer InternalChild = new FillFlowContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[] Children = new Drawable[]
{ {
new Container new Container
@ -134,11 +129,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
} }
} }
}; };
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
background.Colour = colourProvider.Background4; background.Colour = colourProvider.Background4;
controlBackground.Colour = colourProvider.Background5; controlBackground.Colour = colourProvider.Background5;
} }
@ -152,14 +143,11 @@ namespace osu.Game.Overlays.Dashboard.Friends
userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels()); userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels());
} }
public void Fetch() protected override APIRequest<List<User>> CreateRequest() => new GetFriendsRequest();
{
if (!api.IsLoggedIn)
return;
request = new GetFriendsRequest(); protected override void OnSuccess(List<User> response)
request.Success += response => Schedule(() => Users = response); {
api.Queue(request); Users = response;
} }
private void recreatePanels() private void recreatePanels()
@ -258,9 +246,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
request?.Cancel();
cancellationToken?.Cancel(); cancellationToken?.Cancel();
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }
} }

View File

@ -0,0 +1,150 @@
// 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.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Overlays.Dashboard;
using osu.Game.Overlays.Dashboard.Friends;
namespace osu.Game.Overlays
{
public class DashboardOverlay : FullscreenOverlay
{
private CancellationTokenSource cancellationToken;
private Box background;
private Container content;
private DashboardOverlayHeader header;
private LoadingLayer loading;
private OverlayScrollContainer scrollFlow;
public DashboardOverlay()
: base(OverlayColourScheme.Purple)
{
}
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
scrollFlow = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
header = new DashboardOverlayHeader
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Depth = -float.MaxValue
},
content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
}
},
loading = new LoadingLayer(content),
};
background.Colour = ColourProvider.Background5;
}
protected override void LoadComplete()
{
base.LoadComplete();
header.Current.BindValueChanged(onTabChanged);
}
private bool displayUpdateRequired = true;
protected override void PopIn()
{
base.PopIn();
// We don't want to create a new display on every call, only when exiting from fully closed state.
if (displayUpdateRequired)
{
header.Current.TriggerChange();
displayUpdateRequired = false;
}
}
protected override void PopOutComplete()
{
base.PopOutComplete();
loadDisplay(Empty());
displayUpdateRequired = true;
}
private void loadDisplay(Drawable display)
{
scrollFlow.ScrollToStart();
LoadComponentAsync(display, loaded =>
{
if (API.IsLoggedIn)
loading.Hide();
content.Child = loaded;
}, (cancellationToken = new CancellationTokenSource()).Token);
}
private void onTabChanged(ValueChangedEvent<DashboardOverlayTabs> tab)
{
cancellationToken?.Cancel();
loading.Show();
if (!API.IsLoggedIn)
{
loadDisplay(Empty());
return;
}
switch (tab.NewValue)
{
case DashboardOverlayTabs.Friends:
loadDisplay(new FriendDisplay());
break;
default:
throw new NotImplementedException($"Display for {tab.NewValue} tab is not implemented");
}
}
public override void APIStateChanged(IAPIProvider api, APIState state)
{
if (State.Value == Visibility.Hidden)
return;
header.Current.TriggerChange();
}
protected override void Dispose(bool isDisposing)
{
cancellationToken?.Cancel();
base.Dispose(isDisposing);
}
}
}

View File

@ -12,6 +12,8 @@ namespace osu.Game.Overlays
{ {
public abstract class OverlayHeader : Container public abstract class OverlayHeader : Container
{ {
public const int CONTENT_X_MARGIN = 50;
private readonly Box titleBackground; private readonly Box titleBackground;
protected readonly FillFlowContainer HeaderInfo; protected readonly FillFlowContainer HeaderInfo;
@ -54,7 +56,7 @@ namespace osu.Game.Overlays
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding Padding = new MarginPadding
{ {
Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, Horizontal = CONTENT_X_MARGIN,
}, },
Children = new[] Children = new[]
{ {

View File

@ -0,0 +1,79 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API;
namespace osu.Game.Overlays
{
/// <summary>
/// A subview containing online content, to be displayed inside a <see cref="FullscreenOverlay"/>.
/// </summary>
/// <remarks>
/// Automatically performs a data fetch on load.
/// </remarks>
/// <typeparam name="T">The type of the API response.</typeparam>
public abstract class OverlayView<T> : CompositeDrawable, IOnlineComponent
where T : class
{
[Resolved]
protected IAPIProvider API { get; private set; }
private APIRequest<T> request;
protected OverlayView()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
}
protected override void LoadComplete()
{
base.LoadComplete();
API.Register(this);
}
/// <summary>
/// Create the API request for fetching data.
/// </summary>
protected abstract APIRequest<T> CreateRequest();
/// <summary>
/// Fired when results arrive from the main API request.
/// </summary>
/// <param name="response"></param>
protected abstract void OnSuccess(T response);
/// <summary>
/// Force a re-request for data from the API.
/// </summary>
protected void PerformFetch()
{
request?.Cancel();
request = CreateRequest();
request.Success += response => Schedule(() => OnSuccess(response));
API.Queue(request);
}
public virtual void APIStateChanged(IAPIProvider api, APIState state)
{
switch (state)
{
case APIState.Online:
PerformFetch();
break;
}
}
protected override void Dispose(bool isDisposing)
{
request?.Cancel();
API?.Unregister(this);
base.Dispose(isDisposing);
}
}
}

View File

@ -44,7 +44,7 @@ namespace osu.Game.Overlays
}, },
TabControl = CreateTabControl().With(control => TabControl = CreateTabControl().With(control =>
{ {
control.Margin = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN }; control.Margin = new MarginPadding { Left = CONTENT_X_MARGIN };
control.Current = Current; control.Current = Current;
}) })
} }

View File

@ -14,9 +14,9 @@ namespace osu.Game.Overlays.Toolbar
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(SocialOverlay chat) private void load(DashboardOverlay dashboard)
{ {
StateContainer = chat; StateContainer = dashboard;
} }
} }
} }

View File

@ -23,6 +23,13 @@ namespace osu.Game.Rulesets.Mods
protected Bindable<bool> IncreaseFirstObjectVisibility = new Bindable<bool>(); protected Bindable<bool> IncreaseFirstObjectVisibility = new Bindable<bool>();
/// <summary>
/// Check whether the provided hitobject should be considered the "first" hideable object.
/// Can be used to skip spinners, for instance.
/// </summary>
/// <param name="hitObject">The hitobject to check.</param>
protected virtual bool IsFirstHideableObject(DrawableHitObject hitObject) => true;
public void ReadFromConfig(OsuConfigManager config) public void ReadFromConfig(OsuConfigManager config)
{ {
IncreaseFirstObjectVisibility = config.GetBindable<bool>(OsuSetting.IncreaseFirstObjectVisibility); IncreaseFirstObjectVisibility = config.GetBindable<bool>(OsuSetting.IncreaseFirstObjectVisibility);
@ -30,8 +37,11 @@ namespace osu.Game.Rulesets.Mods
public virtual void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables) public virtual void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
{ {
foreach (var d in drawables.Skip(IncreaseFirstObjectVisibility.Value ? 1 : 0)) if (IncreaseFirstObjectVisibility.Value)
d.ApplyCustomUpdateState += ApplyHiddenState; drawables = drawables.SkipWhile(h => !IsFirstHideableObject(h)).Skip(1);
foreach (var dho in drawables)
dho.ApplyCustomUpdateState += ApplyHiddenState;
} }
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)

View File

@ -398,7 +398,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
} }
} }
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => AllJudged && base.UpdateSubTreeMasking(source, maskingBounds); public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false;
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()
{ {

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.Bindables;
using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -12,11 +13,6 @@ namespace osu.Game.Rulesets.Scoring
{ {
public abstract class JudgementProcessor : Component public abstract class JudgementProcessor : Component
{ {
/// <summary>
/// Invoked when all <see cref="HitObject"/>s have been judged by this <see cref="JudgementProcessor"/>.
/// </summary>
public event Action AllJudged;
/// <summary> /// <summary>
/// Invoked when a new judgement has occurred. This occurs after the judgement has been processed by this <see cref="JudgementProcessor"/>. /// Invoked when a new judgement has occurred. This occurs after the judgement has been processed by this <see cref="JudgementProcessor"/>.
/// </summary> /// </summary>
@ -32,10 +28,12 @@ namespace osu.Game.Rulesets.Scoring
/// </summary> /// </summary>
public int JudgedHits { get; private set; } public int JudgedHits { get; private set; }
private readonly BindableBool hasCompleted = new BindableBool();
/// <summary> /// <summary>
/// Whether all <see cref="Judgement"/>s have been processed. /// Whether all <see cref="Judgement"/>s have been processed.
/// </summary> /// </summary>
public bool HasCompleted => JudgedHits == MaxHits; public IBindable<bool> HasCompleted => hasCompleted;
/// <summary> /// <summary>
/// Applies a <see cref="IBeatmap"/> to this <see cref="ScoreProcessor"/>. /// Applies a <see cref="IBeatmap"/> to this <see cref="ScoreProcessor"/>.
@ -60,8 +58,7 @@ namespace osu.Game.Rulesets.Scoring
NewJudgement?.Invoke(result); NewJudgement?.Invoke(result);
if (HasCompleted) updateHasCompleted();
AllJudged?.Invoke();
} }
/// <summary> /// <summary>
@ -72,6 +69,8 @@ namespace osu.Game.Rulesets.Scoring
{ {
JudgedHits--; JudgedHits--;
updateHasCompleted();
RevertResultInternal(result); RevertResultInternal(result);
} }
@ -134,5 +133,7 @@ namespace osu.Game.Rulesets.Scoring
ApplyResult(result); ApplyResult(result);
} }
} }
private void updateHasCompleted() => hasCompleted.Value = JudgedHits == MaxHits;
} }
} }

View File

@ -51,7 +51,7 @@ namespace osu.Game.Screens.Play
isBreakTime.Value = getCurrentBreak()?.HasEffect == true isBreakTime.Value = getCurrentBreak()?.HasEffect == true
|| Clock.CurrentTime < gameplayStartTime || Clock.CurrentTime < gameplayStartTime
|| scoreProcessor?.HasCompleted == true; || scoreProcessor?.HasCompleted.Value == true;
} }
private BreakPeriod getCurrentBreak() private BreakPeriod getCurrentBreak()

View File

@ -37,6 +37,11 @@ namespace osu.Game.Screens.Play
[Cached] [Cached]
public class Player : ScreenWithBeatmapBackground public class Player : ScreenWithBeatmapBackground
{ {
/// <summary>
/// The delay upon completion of the beatmap before displaying the results screen.
/// </summary>
public const double RESULTS_DISPLAY_DELAY = 1000.0;
public override bool AllowBackButton => false; // handled by HoldForMenuButton public override bool AllowBackButton => false; // handled by HoldForMenuButton
protected override UserActivity InitialActivity => new UserActivity.SoloGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); protected override UserActivity InitialActivity => new UserActivity.SoloGame(Beatmap.Value.BeatmapInfo, Ruleset.Value);
@ -197,7 +202,7 @@ namespace osu.Game.Screens.Play
}; };
// Bind the judgement processors to ourselves // Bind the judgement processors to ourselves
ScoreProcessor.AllJudged += onCompletion; ScoreProcessor.HasCompleted.ValueChanged += updateCompletionState;
HealthProcessor.Failed += onFail; HealthProcessor.Failed += onFail;
foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>()) foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>())
@ -412,22 +417,33 @@ namespace osu.Game.Screens.Play
private ScheduledDelegate completionProgressDelegate; private ScheduledDelegate completionProgressDelegate;
private void onCompletion() private void updateCompletionState(ValueChangedEvent<bool> completionState)
{ {
// screen may be in the exiting transition phase. // screen may be in the exiting transition phase.
if (!this.IsCurrentScreen()) if (!this.IsCurrentScreen())
return; return;
if (!completionState.NewValue)
{
completionProgressDelegate?.Cancel();
completionProgressDelegate = null;
ValidForResume = true;
return;
}
if (completionProgressDelegate != null)
throw new InvalidOperationException($"{nameof(updateCompletionState)} was fired more than once");
// Only show the completion screen if the player hasn't failed // Only show the completion screen if the player hasn't failed
if (HealthProcessor.HasFailed || completionProgressDelegate != null) if (HealthProcessor.HasFailed)
return; return;
ValidForResume = false; ValidForResume = false;
if (!showResults) return; if (!showResults) return;
using (BeginDelayedSequence(1000)) using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY))
scheduleGotoRanking(); completionProgressDelegate = Schedule(GotoRanking);
} }
protected virtual ScoreInfo CreateScore() protected virtual ScoreInfo CreateScore()
@ -679,12 +695,6 @@ namespace osu.Game.Screens.Play
storyboardReplacesBackground.Value = false; storyboardReplacesBackground.Value = false;
} }
private void scheduleGotoRanking()
{
completionProgressDelegate?.Cancel();
completionProgressDelegate = Schedule(GotoRanking);
}
#endregion #endregion
} }
} }

View File

@ -28,8 +28,15 @@ namespace osu.Game.Screens.Select
{ {
public class BeatmapCarousel : CompositeDrawable, IKeyBindingHandler<GlobalAction> public class BeatmapCarousel : CompositeDrawable, IKeyBindingHandler<GlobalAction>
{ {
private const float bleed_top = FilterControl.HEIGHT; /// <summary>
private const float bleed_bottom = Footer.HEIGHT; /// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it.
/// </summary>
public float BleedTop { get; set; }
/// <summary>
/// Height of the area below the carousel that should be treated as visible due to transparency of elements in front of it.
/// </summary>
public float BleedBottom { get; set; }
/// <summary> /// <summary>
/// Triggered when the <see cref="BeatmapSets"/> loaded change and are completely loaded. /// Triggered when the <see cref="BeatmapSets"/> loaded change and are completely loaded.
@ -376,17 +383,17 @@ namespace osu.Game.Screens.Select
/// the beatmap carousel bleeds into the <see cref="FilterControl"/> and the <see cref="Footer"/> /// the beatmap carousel bleeds into the <see cref="FilterControl"/> and the <see cref="Footer"/>
/// </remarks> /// </remarks>
/// </summary> /// </summary>
private float visibleHalfHeight => (DrawHeight + bleed_bottom + bleed_top) / 2; private float visibleHalfHeight => (DrawHeight + BleedBottom + BleedTop) / 2;
/// <summary> /// <summary>
/// The position of the lower visible bound with respect to the current scroll position. /// The position of the lower visible bound with respect to the current scroll position.
/// </summary> /// </summary>
private float visibleBottomBound => scroll.Current + DrawHeight + bleed_bottom; private float visibleBottomBound => scroll.Current + DrawHeight + BleedBottom;
/// <summary> /// <summary>
/// The position of the upper visible bound with respect to the current scroll position. /// The position of the upper visible bound with respect to the current scroll position.
/// </summary> /// </summary>
private float visibleUpperBound => scroll.Current - bleed_top; private float visibleUpperBound => scroll.Current - BleedTop;
public void FlushPendingFilterOperations() public void FlushPendingFilterOperations()
{ {
@ -644,7 +651,11 @@ namespace osu.Game.Screens.Select
case DrawableCarouselBeatmap beatmap: case DrawableCarouselBeatmap beatmap:
{ {
if (beatmap.Item.State.Value == CarouselItemState.Selected) if (beatmap.Item.State.Value == CarouselItemState.Selected)
scrollTarget = currentY + beatmap.DrawHeight / 2 - DrawHeight / 2; // scroll position at currentY makes the set panel appear at the very top of the carousel's screen space
// move down by half of visible height (height of the carousel's visible extent, including semi-transparent areas)
// then reapply the top semi-transparent area (because carousel's screen space starts below it)
// and finally add half of the panel's own height to achieve vertical centering of the panel itself
scrollTarget = currentY - visibleHalfHeight + BleedTop + beatmap.DrawHeight / 2;
void performMove(float y, float? startY = null) void performMove(float y, float? startY = null)
{ {

View File

@ -153,6 +153,8 @@ namespace osu.Game.Screens.Select
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
BleedTop = FilterControl.HEIGHT,
BleedBottom = Footer.HEIGHT,
SelectionChanged = updateSelectedBeatmap, SelectionChanged = updateSelectedBeatmap,
BeatmapSetsChanged = carouselBeatmapsLoaded, BeatmapSetsChanged = carouselBeatmapsLoaded,
GetRecommendedBeatmap = recommender.GetRecommendedBeatmap, GetRecommendedBeatmap = recommender.GetRecommendedBeatmap,
@ -426,7 +428,7 @@ namespace osu.Game.Screens.Select
} }
/// <summary> /// <summary>
/// selection has been changed as the result of a user interaction. /// Selection has been changed as the result of a user interaction.
/// </summary> /// </summary>
private void performUpdateSelected() private void performUpdateSelected()
{ {
@ -435,7 +437,7 @@ namespace osu.Game.Screens.Select
selectionChangedDebounce?.Cancel(); selectionChangedDebounce?.Cancel();
if (beatmap == null) if (beatmapNoDebounce == null)
run(); run();
else else
selectionChangedDebounce = Scheduler.AddDelayed(run, 200); selectionChangedDebounce = Scheduler.AddDelayed(run, 200);
@ -448,9 +450,11 @@ namespace osu.Game.Screens.Select
{ {
Mods.Value = Array.Empty<Mod>(); Mods.Value = Array.Empty<Mod>();
// required to return once in order to have the carousel in a good state. // transferRulesetValue() may trigger a refilter. If the current selection does not match the new ruleset, we want to switch away from it.
// if the ruleset changed, the rest of the selection update will happen via updateSelectedRuleset. // The default logic on WorkingBeatmap change is to switch to a matching ruleset (see workingBeatmapChanged()), but we don't want that here.
return; // We perform an early selection attempt and clear out the beatmap selection to avoid a second ruleset change (revert).
if (beatmap != null && !Carousel.SelectBeatmap(beatmap, false))
beatmap = null;
} }
// We may be arriving here due to another component changing the bindable Beatmap. // We may be arriving here due to another component changing the bindable Beatmap.
@ -714,7 +718,7 @@ namespace osu.Game.Screens.Select
if (decoupledRuleset.Value?.Equals(Ruleset.Value) == true) if (decoupledRuleset.Value?.Equals(Ruleset.Value) == true)
return false; return false;
Logger.Log($"decoupled ruleset transferred (\"{decoupledRuleset.Value}\" -> \"{Ruleset.Value}\""); Logger.Log($"decoupled ruleset transferred (\"{decoupledRuleset.Value}\" -> \"{Ruleset.Value}\")");
rulesetNoDebounce = decoupledRuleset.Value = Ruleset.Value; rulesetNoDebounce = decoupledRuleset.Value = Ruleset.Value;
// if we have a pending filter operation, we want to run it now. // if we have a pending filter operation, we want to run it now.

View File

@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual
public bool CheckFailed(bool failed) public bool CheckFailed(bool failed)
{ {
if (!failed) if (!failed)
return ScoreProcessor.HasCompleted && !HealthProcessor.HasFailed; return ScoreProcessor.HasCompleted.Value && !HealthProcessor.HasFailed;
return HealthProcessor.HasFailed; return HealthProcessor.HasFailed;
} }

View File

@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual
public void Flip() => scrollingInfo.Direction.Value = scrollingInfo.Direction.Value == ScrollingDirection.Up ? ScrollingDirection.Down : ScrollingDirection.Up; public void Flip() => scrollingInfo.Direction.Value = scrollingInfo.Direction.Value == ScrollingDirection.Up ? ScrollingDirection.Down : ScrollingDirection.Up;
private class TestScrollingInfo : IScrollingInfo public class TestScrollingInfo : IScrollingInfo
{ {
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>(); public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
IBindable<ScrollingDirection> IScrollingInfo.Direction => Direction; IBindable<ScrollingDirection> IScrollingInfo.Direction => Direction;
@ -54,7 +54,7 @@ namespace osu.Game.Tests.Visual
IScrollAlgorithm IScrollingInfo.Algorithm => Algorithm; IScrollAlgorithm IScrollingInfo.Algorithm => Algorithm;
} }
private class TestScrollAlgorithm : IScrollAlgorithm public class TestScrollAlgorithm : IScrollAlgorithm
{ {
public readonly SortedList<MultiplierControlPoint> ControlPoints = new SortedList<MultiplierControlPoint>(); public readonly SortedList<MultiplierControlPoint> ControlPoints = new SortedList<MultiplierControlPoint>();

View File

@ -19,7 +19,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="DiffPlex" Version="1.6.1" /> <PackageReference Include="DiffPlex" Version="1.6.1" />
<PackageReference Include="Humanizer" Version="2.7.9" /> <PackageReference Include="Humanizer" Version="2.8.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />

View File

@ -76,7 +76,7 @@
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. --> <!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<PackageReference Include="DiffPlex" Version="1.6.1" /> <PackageReference Include="DiffPlex" Version="1.6.1" />
<PackageReference Include="Humanizer" Version="2.7.9" /> <PackageReference Include="Humanizer" Version="2.8.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />