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

Merge branch 'master' into fix-spinner-rpm-user-rate-adjust

This commit is contained in:
Dean Herbert 2020-09-25 13:17:03 +09:00
commit 7df8b5bce7
72 changed files with 1135 additions and 167 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.911.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.923.1" />
</ItemGroup>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Catch.Tests
Schedule(() =>
{
area.AttemptCatch(fruit);
area.OnResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great });
area.OnNewResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great });
drawable.Expire();
});

View File

@ -0,0 +1,65 @@
// 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.Graphics;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneComboCounter : CatchSkinnableTestScene
{
private ScoreProcessor scoreProcessor;
private Color4 judgedObjectColour = Color4.White;
[SetUp]
public void SetUp() => Schedule(() =>
{
scoreProcessor = new ScoreProcessor();
SetContents(() => new CatchComboDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(2.5f),
});
});
[Test]
public void TestCatchComboCounter()
{
AddRepeatStep("perform hit", () => performJudgement(HitResult.Perfect), 20);
AddStep("perform miss", () => performJudgement(HitResult.Miss));
AddStep("randomize judged object colour", () =>
{
judgedObjectColour = new Color4(
RNG.NextSingle(1f),
RNG.NextSingle(1f),
RNG.NextSingle(1f),
1f
);
});
}
private void performJudgement(HitResult type, Judgement judgement = null)
{
var judgedObject = new DrawableFruit(new Fruit()) { AccentColour = { Value = judgedObjectColour } };
var result = new JudgementResult(judgedObject.HitObject, judgement ?? new Judgement()) { Type = type };
scoreProcessor.ApplyResult(result);
foreach (var counter in CreatedDrawables.Cast<CatchComboDisplay>())
counter.OnNewResult(judgedObject, result);
}
}
}

View File

@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Catch
Droplet,
CatcherIdle,
CatcherFail,
CatcherKiai
CatcherKiai,
CatchComboCounter
}
}

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Rulesets.Catch.Skinning
{
@ -52,6 +53,15 @@ namespace osu.Game.Rulesets.Catch.Skinning
case CatchSkinComponents.CatcherKiai:
return this.GetAnimation("fruit-catcher-kiai", true, true, true) ??
this.GetAnimation("fruit-ryuuta", true, true, true);
case CatchSkinComponents.CatchComboCounter:
var comboFont = GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
if (this.HasFont(comboFont))
return new LegacyComboCounter(Source);
break;
}
return null;

View File

@ -0,0 +1,103 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Rulesets.Catch.Skinning
{
/// <summary>
/// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter.
/// </summary>
public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter
{
private readonly LegacyRollingCounter counter;
private readonly LegacyRollingCounter explosion;
public LegacyComboCounter(ISkin skin)
{
var fontName = skin.GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
var fontOverlap = skin.GetConfig<LegacySetting, float>(LegacySetting.ComboOverlap)?.Value ?? -2f;
AutoSizeAxes = Axes.Both;
Alpha = 0f;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Scale = new Vector2(0.8f);
InternalChildren = new Drawable[]
{
explosion = new LegacyRollingCounter(skin, fontName, fontOverlap)
{
Alpha = 0.65f,
Blending = BlendingParameters.Additive,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(1.5f),
},
counter = new LegacyRollingCounter(skin, fontName, fontOverlap)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
};
}
private int lastDisplayedCombo;
public void UpdateCombo(int combo, Color4? hitObjectColour = null)
{
if (combo == lastDisplayedCombo)
return;
// There may still be existing transforms to the counter (including value change after 250ms),
// finish them immediately before new transforms.
counter.SetCountWithoutRolling(lastDisplayedCombo);
lastDisplayedCombo = combo;
if (Time.Elapsed < 0)
{
// needs more work to make rewind somehow look good.
// basically we want the previous increment to play... or turning off RemoveCompletedTransforms (not feasible from a performance angle).
Hide();
return;
}
// Combo fell to zero, roll down and fade out the counter.
if (combo == 0)
{
counter.Current.Value = 0;
explosion.Current.Value = 0;
this.FadeOut(400, Easing.Out);
}
else
{
this.FadeInFromZero().Then().Delay(1000).FadeOut(300);
counter.ScaleTo(1.5f)
.ScaleTo(0.8f, 250, Easing.Out)
.OnComplete(c => c.SetCountWithoutRolling(combo));
counter.Delay(250)
.ScaleTo(1f)
.ScaleTo(1.1f, 60).Then().ScaleTo(1f, 30);
explosion.Colour = hitObjectColour ?? Color4.White;
explosion.SetCountWithoutRolling(combo);
explosion.ScaleTo(1.5f)
.ScaleTo(1.9f, 400, Easing.Out)
.FadeOutFromOne(400);
}
}
}
}

View File

@ -0,0 +1,62 @@
// 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 JetBrains.Annotations;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.UI
{
/// <summary>
/// Represents a component that displays a skinned <see cref="ICatchComboCounter"/> and handles combo judgement results for updating it accordingly.
/// </summary>
public class CatchComboDisplay : SkinnableDrawable
{
private int currentCombo;
[CanBeNull]
public ICatchComboCounter ComboCounter => Drawable as ICatchComboCounter;
public CatchComboDisplay()
: base(new CatchSkinComponent(CatchSkinComponents.CatchComboCounter), _ => Empty())
{
}
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
base.SkinChanged(skin, allowFallback);
ComboCounter?.UpdateCombo(currentCombo);
}
public void OnNewResult(DrawableCatchHitObject judgedObject, JudgementResult result)
{
if (!result.Judgement.AffectsCombo || !result.HasResult)
return;
if (result.Type == HitResult.Miss)
{
updateCombo(0, null);
return;
}
updateCombo(result.ComboAtJudgement + 1, judgedObject.AccentColour.Value);
}
public void OnRevertResult(DrawableCatchHitObject judgedObject, JudgementResult result)
{
if (!result.Judgement.AffectsCombo || !result.HasResult)
return;
updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value);
}
private void updateCombo(int newCombo, Color4? hitObjectColour)
{
currentCombo = newCombo;
ComboCounter?.UpdateCombo(newCombo, hitObjectColour);
}
}
}

View File

@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.UI
explodingFruitContainer,
CatcherArea.MovableCatcher.CreateProxiedContent(),
HitObjectContainer,
CatcherArea
CatcherArea,
};
}
@ -62,6 +62,7 @@ namespace osu.Game.Rulesets.Catch.UI
public override void Add(DrawableHitObject h)
{
h.OnNewResult += onNewResult;
h.OnRevertResult += onRevertResult;
base.Add(h);
@ -70,6 +71,9 @@ namespace osu.Game.Rulesets.Catch.UI
}
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
=> CatcherArea.OnResult((DrawableCatchHitObject)judgedObject, result);
=> CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result);
private void onRevertResult(DrawableHitObject judgedObject, JudgementResult result)
=> CatcherArea.OnRevertResult((DrawableCatchHitObject)judgedObject, result);
}
}

View File

@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Catch.UI
public Func<CatchHitObject, DrawableHitObject<CatchHitObject>> CreateDrawableRepresentation;
public readonly Catcher MovableCatcher;
private readonly CatchComboDisplay comboDisplay;
public Container ExplodingFruitTarget
{
@ -34,10 +35,22 @@ namespace osu.Game.Rulesets.Catch.UI
public CatcherArea(BeatmapDifficulty difficulty = null)
{
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
Child = MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X };
Children = new Drawable[]
{
comboDisplay = new CatchComboDisplay
{
RelativeSizeAxes = Axes.None,
AutoSizeAxes = Axes.Both,
Anchor = Anchor.TopLeft,
Origin = Anchor.Centre,
Margin = new MarginPadding { Bottom = 350f },
X = CatchPlayfield.CENTER_X
},
MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X },
};
}
public void OnResult(DrawableCatchHitObject fruit, JudgementResult result)
public void OnNewResult(DrawableCatchHitObject fruit, JudgementResult result)
{
if (result.Judgement is IgnoreJudgement)
return;
@ -86,8 +99,13 @@ namespace osu.Game.Rulesets.Catch.UI
else
MovableCatcher.Drop();
}
comboDisplay.OnNewResult(fruit, result);
}
public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result)
=> comboDisplay.OnRevertResult(fruit, result);
public void OnReleased(CatchAction action)
{
}
@ -105,6 +123,8 @@ namespace osu.Game.Rulesets.Catch.UI
if (state?.CatcherX != null)
MovableCatcher.X = state.CatcherX.Value;
comboDisplay.X = MovableCatcher.X;
}
}
}

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.
using osu.Framework.Graphics;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.UI
{
/// <summary>
/// An interface providing a set of methods to update the combo counter.
/// </summary>
public interface ICatchComboCounter : IDrawable
{
/// <summary>
/// Updates the counter to animate a transition from the old combo value it had to the current provided one.
/// </summary>
/// <remarks>
/// This is called regardless of whether the clock is rewinding.
/// </remarks>
/// <param name="combo">The new combo value.</param>
/// <param name="hitObjectColour">The colour of the object if hit, null on miss.</param>
void UpdateCombo(int combo, Color4? hitObjectColour = null);
}
}

View File

@ -34,11 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components
Alpha = 0.5f,
Child = new Box { RelativeSizeAxes = Axes.Both }
},
ring = new RingPiece
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
}
ring = new RingPiece()
};
}

View File

@ -46,14 +46,24 @@ namespace osu.Game.Rulesets.Osu.Edit
distanceSnapToggle
};
private BindableList<HitObject> selectedHitObjects;
private Bindable<HitObject> placementObject;
[BackgroundDependencyLoader]
private void load()
{
LayerBelowRuleset.Add(distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both });
EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();
EditorBeatmap.PlacementObject.ValueChanged += _ => updateDistanceSnapGrid();
selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy();
selectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();
placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
placementObject.ValueChanged += _ => updateDistanceSnapGrid();
distanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid();
// we may be entering the screen with a selection already active
updateDistanceSnapGrid();
}
protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable<DrawableHitObject> hitObjects)

View File

@ -46,7 +46,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
private void addConnection(FollowPointConnection connection)
{
// Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections
int index = connections.AddInPlace(connection, Comparer<FollowPointConnection>.Create((g1, g2) => g1.StartTime.Value.CompareTo(g2.StartTime.Value)));
int index = connections.AddInPlace(connection, Comparer<FollowPointConnection>.Create((g1, g2) =>
{
int comp = g1.StartTime.Value.CompareTo(g2.StartTime.Value);
if (comp != 0)
return comp;
// we always want to insert the new item after equal ones.
// this is important for beatmaps with multiple hitobjects at the same point in time.
// if we use standard comparison insert order, there will be a churn of connections getting re-updated to
// the next object at the point-in-time, adding a construction/disposal overhead (see FollowPointConnection.End implementation's ClearInternal).
// this is easily visible on https://osu.ppy.sh/beatmapsets/150945#osu/372245
return -1;
}));
if (index < connections.Count - 1)
{

View File

@ -9,7 +9,7 @@ using osu.Framework.Graphics.Shapes;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class RingPiece : Container
public class RingPiece : CircularContainer
{
public RingPiece()
{
@ -18,21 +18,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
InternalChild = new CircularContainer
Masking = true;
BorderThickness = 10;
BorderColour = Color4.White;
Child = new Box
{
Masking = true,
BorderThickness = 10,
BorderColour = Color4.White,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
AlwaysPresent = true,
Alpha = 0,
RelativeSizeAxes = Axes.Both
}
}
AlwaysPresent = true,
Alpha = 0,
RelativeSizeAxes = Axes.Both
};
}
}

View File

@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
var font = GetConfig<OsuSkinConfiguration, string>(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default";
var overlap = GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? -2;
return !hasFont(font)
return !this.HasFont(font)
? null
: new LegacySpriteText(Source, font)
{
@ -145,7 +145,5 @@ namespace osu.Game.Rulesets.Osu.Skinning
return Source.GetConfig<TLookup, TValue>(lookup);
}
private bool hasFont(string fontName) => Source.GetTexture($"{fontName}-0") != null;
}
}

View File

@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.Tests
{
public class DrawableTestStrongHit : DrawableHit
{
private readonly HitResult type;
private readonly bool hitBoth;
public DrawableTestStrongHit(double startTime, HitResult type = HitResult.Great, bool hitBoth = true)
: base(new Hit
{
IsStrong = true,
StartTime = startTime,
})
{
// in order to create nested strong hit
HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
this.type = type;
this.hitBoth = hitBoth;
}
protected override void LoadAsyncComplete()
{
base.LoadAsyncComplete();
Result.Type = type;
var nestedStrongHit = (DrawableStrongNestedHit)NestedHitObjects.Single();
nestedStrongHit.Result.Type = hitBoth ? type : HitResult.Miss;
}
public override bool OnPressed(TaikoAction action) => false;
}
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Scoring;
@ -15,24 +14,29 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[TestFixture]
public class TestSceneHitExplosion : TaikoSkinnableTestScene
{
[BackgroundDependencyLoader]
private void load()
[Test]
public void TestNormalHit()
{
AddStep("Great", () => SetContents(() => getContentFor(HitResult.Great)));
AddStep("Good", () => SetContents(() => getContentFor(HitResult.Good)));
AddStep("Miss", () => SetContents(() => getContentFor(HitResult.Miss)));
AddStep("Great", () => SetContents(() => getContentFor(createHit(HitResult.Great))));
AddStep("Good", () => SetContents(() => getContentFor(createHit(HitResult.Good))));
AddStep("Miss", () => SetContents(() => getContentFor(createHit(HitResult.Miss))));
}
private Drawable getContentFor(HitResult type)
[Test]
public void TestStrongHit([Values(false, true)] bool hitBoth)
{
DrawableTaikoHitObject hit;
AddStep("Great", () => SetContents(() => getContentFor(createStrongHit(HitResult.Great, hitBoth))));
AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Good, hitBoth))));
}
private Drawable getContentFor(DrawableTaikoHitObject hit)
{
return new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
hit = createHit(type),
hit,
new HitExplosion(hit)
{
Anchor = Anchor.Centre,
@ -43,5 +47,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
}
private DrawableTaikoHitObject createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type);
private DrawableTaikoHitObject createStrongHit(HitResult type, bool hitBoth)
=> new DrawableTestStrongHit(Time.Current, type, hitBoth);
}
}

View File

@ -174,7 +174,9 @@ namespace osu.Game.Rulesets.Taiko.Tests
private void addMissJudgement()
{
((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(new DrawableTestHit(new Hit()), new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss });
DrawableTestHit h;
Add(h = new DrawableTestHit(new Hit(), HitResult.Miss));
((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss });
}
private void addBarLine(bool major, double delay = scroll_time)

View File

@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
yield return new TernaryStateMenuItem("Rim", action: state =>
{
ChangeHandler.BeginChange();
foreach (var h in hits)
{
switch (state)
@ -35,6 +37,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
break;
}
}
ChangeHandler.EndChange();
})
{
State = { Value = getTernaryState(hits, h => h.Type == HitType.Rim) }
@ -47,6 +51,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
yield return new TernaryStateMenuItem("Strong", action: state =>
{
ChangeHandler.BeginChange();
foreach (var h in hits)
{
switch (state)
@ -62,6 +68,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
EditorBeatmap?.UpdateHitObject(h);
}
ChangeHandler.EndChange();
})
{
State = { Value = getTernaryState(hits, h => h.IsStrong) }

View File

@ -75,7 +75,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning
return null;
case TaikoSkinComponents.TaikoExplosionGood:
case TaikoSkinComponents.TaikoExplosionGoodStrong:
case TaikoSkinComponents.TaikoExplosionGreat:
case TaikoSkinComponents.TaikoExplosionGreatStrong:
case TaikoSkinComponents.TaikoExplosionMiss:
var sprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
@ -107,8 +109,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning
case TaikoSkinComponents.TaikoExplosionGood:
return "taiko-hit100";
case TaikoSkinComponents.TaikoExplosionGoodStrong:
return "taiko-hit100k";
case TaikoSkinComponents.TaikoExplosionGreat:
return "taiko-hit300";
case TaikoSkinComponents.TaikoExplosionGreatStrong:
return "taiko-hit300k";
}
throw new ArgumentOutOfRangeException(nameof(component), "Invalid result type");

View File

@ -17,7 +17,9 @@ namespace osu.Game.Rulesets.Taiko
BarLine,
TaikoExplosionMiss,
TaikoExplosionGood,
TaikoExplosionGoodStrong,
TaikoExplosionGreat,
TaikoExplosionGreatStrong,
Scroller,
Mascot,
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osuTK;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@ -9,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.UI
@ -45,24 +47,41 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader]
private void load()
{
Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject.Result?.Type ?? HitResult.Great)), _ => new DefaultHitExplosion());
Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject)), _ => new DefaultHitExplosion());
}
private TaikoSkinComponents getComponentName(HitResult resultType)
private TaikoSkinComponents getComponentName(DrawableHitObject judgedObject)
{
var resultType = judgedObject.Result?.Type ?? HitResult.Great;
switch (resultType)
{
case HitResult.Miss:
return TaikoSkinComponents.TaikoExplosionMiss;
case HitResult.Good:
return TaikoSkinComponents.TaikoExplosionGood;
return useStrongExplosion(judgedObject)
? TaikoSkinComponents.TaikoExplosionGoodStrong
: TaikoSkinComponents.TaikoExplosionGood;
case HitResult.Great:
return TaikoSkinComponents.TaikoExplosionGreat;
return useStrongExplosion(judgedObject)
? TaikoSkinComponents.TaikoExplosionGreatStrong
: TaikoSkinComponents.TaikoExplosionGreat;
}
throw new ArgumentOutOfRangeException(nameof(resultType), "Invalid result type");
throw new ArgumentOutOfRangeException(nameof(judgedObject), "Invalid result type");
}
private bool useStrongExplosion(DrawableHitObject judgedObject)
{
if (!(judgedObject.HitObject is Hit))
return false;
if (!(judgedObject.NestedHitObjects.SingleOrDefault() is DrawableStrongNestedHit nestedHit))
return false;
return judgedObject.Result.Type == nestedHit.Result.Type;
}
/// <summary>

View File

@ -205,9 +205,6 @@ namespace osu.Game.Rulesets.Taiko.UI
X = result.IsHit ? judgedObject.Position.X : 0,
});
if (!result.IsHit)
break;
var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre;
addExplosion(judgedObject, type);
@ -218,12 +215,16 @@ namespace osu.Game.Rulesets.Taiko.UI
private void addDrumRollHit(DrawableDrumRollTick drawableTick) =>
drumRollHitContainer.Add(new DrawableFlyingHit(drawableTick));
private void addExplosion(DrawableHitObject drawableObject, HitType type)
/// <remarks>
/// As legacy skins have different explosions for singular and double strong hits,
/// explosion addition is scheduled to ensure that both hits are processed if they occur on the same frame.
/// </remarks>
private void addExplosion(DrawableHitObject drawableObject, HitType type) => Schedule(() =>
{
hitExplosionContainer.Add(new HitExplosion(drawableObject));
if (drawableObject.HitObject.Kiai)
kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type));
}
});
private class ProxyContainer : LifetimeManagementContainer
{

View File

@ -81,7 +81,7 @@ namespace osu.Game.Tests.Gameplay
private class TestHitObjectWithCombo : ConvertHitObject, IHasComboInformation
{
public bool NewCombo { get; } = false;
public bool NewCombo { get; set; } = false;
public int ComboOffset { get; } = 0;
public Bindable<int> IndexInCurrentComboBindable { get; } = new Bindable<int>();

View File

@ -0,0 +1,134 @@
// 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.IO;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Rulesets
{
public class TestSceneDrawableRulesetDependencies : OsuTestScene
{
[Test]
public void TestDisposalDoesNotDisposeParentStores()
{
DrawableWithDependencies drawable = null;
TestTextureStore textureStore = null;
TestSampleStore sampleStore = null;
AddStep("add dependencies", () =>
{
Child = drawable = new DrawableWithDependencies();
textureStore = drawable.ParentTextureStore;
sampleStore = drawable.ParentSampleStore;
});
AddStep("clear children", Clear);
AddUntilStep("wait for disposal", () => drawable.IsDisposed);
AddStep("GC", () =>
{
drawable = null;
GC.Collect();
GC.WaitForPendingFinalizers();
});
AddAssert("parent texture store not disposed", () => !textureStore.IsDisposed);
AddAssert("parent sample store not disposed", () => !sampleStore.IsDisposed);
}
private class DrawableWithDependencies : CompositeDrawable
{
public TestTextureStore ParentTextureStore { get; private set; }
public TestSampleStore ParentSampleStore { get; private set; }
public DrawableWithDependencies()
{
InternalChild = new Box { RelativeSizeAxes = Axes.Both };
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs<TextureStore>(ParentTextureStore = new TestTextureStore());
dependencies.CacheAs<ISampleStore>(ParentSampleStore = new TestSampleStore());
return new DrawableRulesetDependencies(new OsuRuleset(), dependencies);
}
public new bool IsDisposed { get; private set; }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
IsDisposed = true;
}
}
private class TestTextureStore : TextureStore
{
public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => null;
public bool IsDisposed { get; private set; }
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
IsDisposed = true;
}
}
private class TestSampleStore : ISampleStore
{
public bool IsDisposed { get; private set; }
public void Dispose()
{
IsDisposed = true;
}
public SampleChannel Get(string name) => null;
public Task<SampleChannel> GetAsync(string name) => null;
public Stream GetStream(string name) => null;
public IEnumerable<string> GetAvailableResources() => throw new NotImplementedException();
public BindableNumber<double> Volume => throw new NotImplementedException();
public BindableNumber<double> Balance => throw new NotImplementedException();
public BindableNumber<double> Frequency => throw new NotImplementedException();
public BindableNumber<double> Tempo => throw new NotImplementedException();
public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotImplementedException();
public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotImplementedException();
public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotImplementedException();
public IBindable<double> AggregateVolume => throw new NotImplementedException();
public IBindable<double> AggregateBalance => throw new NotImplementedException();
public IBindable<double> AggregateFrequency => throw new NotImplementedException();
public IBindable<double> AggregateTempo => throw new NotImplementedException();
public int PlaybackConcurrency { get; set; }
}
}
}

View File

@ -1,12 +1,17 @@
// 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.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Audio;
@ -20,6 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached]
private GameplayClock gameplayClock = new GameplayClock(new FramedClock());
private TestSkinSourceContainer skinSource;
private SkinnableSound skinnableSound;
[SetUp]
@ -29,7 +35,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Children = new Drawable[]
{
new Container
skinSource = new TestSkinSourceContainer
{
Clock = gameplayClock,
RelativeSizeAxes = Axes.Both,
@ -101,5 +107,55 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("sample not playing", () => !sample.Playing);
AddAssert("sample not playing", () => !sample.Playing);
}
[Test]
public void TestSkinChangeDoesntPlayOnPause()
{
DrawableSample sample = null;
AddStep("start sample", () =>
{
skinnableSound.Play();
sample = skinnableSound.ChildrenOfType<DrawableSample>().Single();
});
AddAssert("sample playing", () => sample.Playing);
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
AddStep("trigger skin change", () => skinSource.TriggerSourceChanged());
AddAssert("retrieve and ensure current sample is different", () =>
{
DrawableSample oldSample = sample;
sample = skinnableSound.ChildrenOfType<DrawableSample>().Single();
return sample != oldSample;
});
AddAssert("new sample stopped", () => !sample.Playing);
AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
AddWaitStep("wait a bit", 5);
AddAssert("new sample not played", () => !sample.Playing);
}
[Cached(typeof(ISkinSource))]
private class TestSkinSourceContainer : Container, ISkinSource
{
[Resolved]
private ISkinSource source { get; set; }
public event Action SourceChanged;
public Drawable GetDrawableComponent(ISkinComponent component) => source?.GetDrawableComponent(component);
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT);
public SampleChannel GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => source?.GetConfig<TLookup, TValue>(lookup);
public void TriggerSourceChanged()
{
SourceChanged?.Invoke();
}
}
}
}

View File

@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.Online
public void TestMultipleLoads()
{
var comments = exampleComments;
int topLevelCommentCount = exampleComments.Comments.Count(comment => comment.IsTopLevel);
int topLevelCommentCount = exampleComments.Comments.Count;
AddStep("hide container", () => commentsContainer.Hide());
setUpCommentsResponse(comments);

View File

@ -220,7 +220,7 @@ namespace osu.Game.Tests.Visual.Ranking
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddAssert("download button is disabled", () => !screen.ChildrenOfType<DownloadButton>().Single().Enabled.Value);
AddAssert("download button is disabled", () => !screen.ChildrenOfType<DownloadButton>().Last().Enabled.Value);
AddStep("click contracted panel", () =>
{
@ -229,7 +229,7 @@ namespace osu.Game.Tests.Visual.Ranking
InputManager.Click(MouseButton.Left);
});
AddAssert("download button is enabled", () => screen.ChildrenOfType<DownloadButton>().Single().Enabled.Value);
AddAssert("download button is enabled", () => screen.ChildrenOfType<DownloadButton>().Last().Enabled.Value);
}
private class TestResultsContainer : Container

View File

@ -21,7 +21,7 @@ namespace osu.Game.Input.Bindings
handler = game;
}
public override IEnumerable<KeyBinding> DefaultKeyBindings => GlobalKeyBindings.Concat(InGameKeyBindings).Concat(AudioControlKeyBindings);
public override IEnumerable<KeyBinding> DefaultKeyBindings => GlobalKeyBindings.Concat(InGameKeyBindings).Concat(AudioControlKeyBindings).Concat(EditorKeyBindings);
public IEnumerable<KeyBinding> GlobalKeyBindings => new[]
{
@ -50,6 +50,14 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.KeypadEnter, GlobalAction.Select),
};
public IEnumerable<KeyBinding> EditorKeyBindings => new[]
{
new KeyBinding(new[] { InputKey.F1 }, GlobalAction.EditorComposeMode),
new KeyBinding(new[] { InputKey.F2 }, GlobalAction.EditorDesignMode),
new KeyBinding(new[] { InputKey.F3 }, GlobalAction.EditorTimingMode),
new KeyBinding(new[] { InputKey.F4 }, GlobalAction.EditorSetupMode),
};
public IEnumerable<KeyBinding> InGameKeyBindings => new[]
{
new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene),
@ -68,7 +76,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Alt, InputKey.Down }, GlobalAction.DecreaseVolume),
new KeyBinding(new[] { InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.DecreaseVolume),
new KeyBinding(InputKey.F4, GlobalAction.ToggleMute),
new KeyBinding(new[] { InputKey.Control, InputKey.F4 }, GlobalAction.ToggleMute),
new KeyBinding(InputKey.TrackPrevious, GlobalAction.MusicPrev),
new KeyBinding(InputKey.F1, GlobalAction.MusicPrev),
@ -139,7 +147,7 @@ namespace osu.Game.Input.Bindings
[Description("Quick exit (Hold)")]
QuickExit,
// Game-wide beatmap msi ccotolle keybindings
// Game-wide beatmap music controller keybindings
[Description("Next track")]
MusicNext,
@ -166,5 +174,18 @@ namespace osu.Game.Input.Bindings
[Description("Pause")]
PauseGameplay,
// Editor
[Description("Setup Mode")]
EditorSetupMode,
[Description("Compose Mode")]
EditorComposeMode,
[Description("Design Mode")]
EditorDesignMode,
[Description("Timing Mode")]
EditorTimingMode,
}
}

View File

@ -22,6 +22,7 @@ namespace osu.Game.Overlays.KeyBinding
Add(new DefaultBindingsSubsection(manager));
Add(new AudioControlKeyBindingsSubsection(manager));
Add(new InGameKeyBindingsSubsection(manager));
Add(new EditorKeyBindingsSubsection(manager));
}
private class DefaultBindingsSubsection : KeyBindingsSubsection
@ -56,5 +57,16 @@ namespace osu.Game.Overlays.KeyBinding
Defaults = manager.AudioControlKeyBindings;
}
}
private class EditorKeyBindingsSubsection : KeyBindingsSubsection
{
protected override string Header => "Editor";
public EditorKeyBindingsSubsection(GlobalActionContainer manager)
: base(null)
{
Defaults = manager.EditorKeyBindings;
}
}
}
}

View File

@ -58,6 +58,8 @@ namespace osu.Game.Rulesets.Edit
private RadioButtonCollection toolboxCollection;
private ToolboxGroup togglesCollection;
protected HitObjectComposer(Ruleset ruleset)
{
Ruleset = ruleset;
@ -78,7 +80,7 @@ namespace osu.Game.Rulesets.Edit
}
catch (Exception e)
{
Logger.Error(e, "Could not load beatmap sucessfully!");
Logger.Error(e, "Could not load beatmap successfully!");
return;
}
@ -115,7 +117,7 @@ namespace osu.Game.Rulesets.Edit
Children = new Drawable[]
{
new ToolboxGroup("toolbox") { Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } },
new ToolboxGroup("toggles")
togglesCollection = new ToolboxGroup("toggles")
{
ChildrenEnumerable = Toggles.Select(b => new SettingsCheckbox
{
@ -190,9 +192,9 @@ namespace osu.Game.Rulesets.Edit
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Key >= Key.Number1 && e.Key <= Key.Number9)
if (checkLeftToggleFromKey(e.Key, out var leftIndex))
{
var item = toolboxCollection.Items.ElementAtOrDefault(e.Key - Key.Number1);
var item = toolboxCollection.Items.ElementAtOrDefault(leftIndex);
if (item != null)
{
@ -201,9 +203,84 @@ namespace osu.Game.Rulesets.Edit
}
}
if (checkRightToggleFromKey(e.Key, out var rightIndex))
{
var item = togglesCollection.Children[rightIndex];
if (item is SettingsCheckbox checkbox)
{
checkbox.Bindable.Value = !checkbox.Bindable.Value;
return true;
}
}
return base.OnKeyDown(e);
}
private bool checkLeftToggleFromKey(Key key, out int index)
{
if (key < Key.Number1 || key > Key.Number9)
{
index = -1;
return false;
}
index = key - Key.Number1;
return true;
}
private bool checkRightToggleFromKey(Key key, out int index)
{
switch (key)
{
case Key.Q:
index = 0;
break;
case Key.W:
index = 1;
break;
case Key.E:
index = 2;
break;
case Key.R:
index = 3;
break;
case Key.T:
index = 4;
break;
case Key.Y:
index = 5;
break;
case Key.U:
index = 6;
break;
case Key.I:
index = 7;
break;
case Key.O:
index = 8;
break;
case Key.P:
index = 9;
break;
default:
index = -1;
break;
}
return index >= 0;
}
private void selectionChanged(object sender, NotifyCollectionChangedEventArgs changedArgs)
{
if (EditorBeatmap.SelectedHitObjects.Any())

View File

@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.Objects.Types
/// </summary>
int ComboIndex { get; set; }
/// <summary>
/// Whether the HitObject starts a new combo.
/// </summary>
new bool NewCombo { get; set; }
Bindable<bool> LastInComboBindable { get; }
/// <summary>

View File

@ -10,6 +10,7 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Game.Rulesets.Configuration;
@ -46,12 +47,11 @@ namespace osu.Game.Rulesets.UI
if (resources != null)
{
TextureStore = new TextureStore(new TextureLoaderStore(new NamespacedResourceStore<byte[]>(resources, @"Textures")));
TextureStore.AddStore(parent.Get<TextureStore>());
Cache(TextureStore);
CacheAs(TextureStore = new FallbackTextureStore(TextureStore, parent.Get<TextureStore>()));
SampleStore = parent.Get<AudioManager>().GetSampleStore(new NamespacedResourceStore<byte[]>(resources, @"Samples"));
SampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY;
CacheAs<ISampleStore>(new FallbackSampleStore(SampleStore, parent.Get<ISampleStore>()));
CacheAs(SampleStore = new FallbackSampleStore(SampleStore, parent.Get<ISampleStore>()));
}
RulesetConfigManager = parent.Get<RulesetConfigCache>().GetConfigFor(ruleset);
@ -82,69 +82,92 @@ namespace osu.Game.Rulesets.UI
isDisposed = true;
SampleStore?.Dispose();
TextureStore?.Dispose();
RulesetConfigManager = null;
}
#endregion
}
/// <summary>
/// A sample store which adds a fallback source.
/// </summary>
/// <remarks>
/// This is a temporary implementation to workaround ISampleStore limitations.
/// </remarks>
public class FallbackSampleStore : ISampleStore
{
private readonly ISampleStore primary;
private readonly ISampleStore secondary;
public FallbackSampleStore(ISampleStore primary, ISampleStore secondary)
/// <summary>
/// A sample store which adds a fallback source and prevents disposal of the fallback source.
/// </summary>
private class FallbackSampleStore : ISampleStore
{
this.primary = primary;
this.secondary = secondary;
private readonly ISampleStore primary;
private readonly ISampleStore fallback;
public FallbackSampleStore(ISampleStore primary, ISampleStore fallback)
{
this.primary = primary;
this.fallback = fallback;
}
public SampleChannel Get(string name) => primary.Get(name) ?? fallback.Get(name);
public Task<SampleChannel> GetAsync(string name) => primary.GetAsync(name) ?? fallback.GetAsync(name);
public Stream GetStream(string name) => primary.GetStream(name) ?? fallback.GetStream(name);
public IEnumerable<string> GetAvailableResources() => throw new NotSupportedException();
public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotSupportedException();
public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotSupportedException();
public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotSupportedException();
public BindableNumber<double> Volume => throw new NotSupportedException();
public BindableNumber<double> Balance => throw new NotSupportedException();
public BindableNumber<double> Frequency => throw new NotSupportedException();
public BindableNumber<double> Tempo => throw new NotSupportedException();
public IBindable<double> GetAggregate(AdjustableProperty type) => throw new NotSupportedException();
public IBindable<double> AggregateVolume => throw new NotSupportedException();
public IBindable<double> AggregateBalance => throw new NotSupportedException();
public IBindable<double> AggregateFrequency => throw new NotSupportedException();
public IBindable<double> AggregateTempo => throw new NotSupportedException();
public int PlaybackConcurrency
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public void Dispose()
{
primary?.Dispose();
}
}
public SampleChannel Get(string name) => primary.Get(name) ?? secondary.Get(name);
public Task<SampleChannel> GetAsync(string name) => primary.GetAsync(name) ?? secondary.GetAsync(name);
public Stream GetStream(string name) => primary.GetStream(name) ?? secondary.GetStream(name);
public IEnumerable<string> GetAvailableResources() => throw new NotSupportedException();
public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotSupportedException();
public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotSupportedException();
public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotSupportedException();
public BindableNumber<double> Volume => throw new NotSupportedException();
public BindableNumber<double> Balance => throw new NotSupportedException();
public BindableNumber<double> Frequency => throw new NotSupportedException();
public BindableNumber<double> Tempo => throw new NotSupportedException();
public IBindable<double> GetAggregate(AdjustableProperty type) => throw new NotSupportedException();
public IBindable<double> AggregateVolume => throw new NotSupportedException();
public IBindable<double> AggregateBalance => throw new NotSupportedException();
public IBindable<double> AggregateFrequency => throw new NotSupportedException();
public IBindable<double> AggregateTempo => throw new NotSupportedException();
public int PlaybackConcurrency
/// <summary>
/// A texture store which adds a fallback source and prevents disposal of the fallback source.
/// </summary>
private class FallbackTextureStore : TextureStore
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
private readonly TextureStore primary;
private readonly TextureStore fallback;
public void Dispose()
{
public FallbackTextureStore(TextureStore primary, TextureStore fallback)
{
this.primary = primary;
this.fallback = fallback;
}
public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT)
=> primary.Get(name, wrapModeS, wrapModeT) ?? fallback.Get(name, wrapModeS, wrapModeT);
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
primary?.Dispose();
}
}
}
}

View File

@ -43,10 +43,20 @@ namespace osu.Game.Rulesets.UI
return true;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
unbindStartTimeMap();
}
public virtual void Clear(bool disposeChildren = true)
{
ClearInternal(disposeChildren);
unbindStartTimeMap();
}
private void unbindStartTimeMap()
{
foreach (var kvp in startTimeMap)
kvp.Value.bindable.UnbindAll();
startTimeMap.Clear();

View File

@ -37,7 +37,21 @@ namespace osu.Game.Rulesets.UI
/// <summary>
/// All the <see cref="DrawableHitObject"/>s contained in this <see cref="Playfield"/> and all <see cref="NestedPlayfields"/>.
/// </summary>
public IEnumerable<DrawableHitObject> AllHitObjects => HitObjectContainer?.Objects.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects)) ?? Enumerable.Empty<DrawableHitObject>();
public IEnumerable<DrawableHitObject> AllHitObjects
{
get
{
if (HitObjectContainer == null)
return Enumerable.Empty<DrawableHitObject>();
var enumerable = HitObjectContainer.Objects;
if (nestedPlayfields.IsValueCreated)
enumerable = enumerable.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects));
return enumerable;
}
}
/// <summary>
/// All <see cref="Playfield"/>s nested inside this <see cref="Playfield"/>.

View File

@ -22,10 +22,12 @@ namespace osu.Game.Screens.Edit.Components
{
trackTimer = new OsuSpriteText
{
Origin = Anchor.BottomLeft,
RelativePositionAxes = Axes.Y,
Font = OsuFont.GetFont(size: 22, fixedWidth: true),
Y = 0.5f,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
// intentionally fudged centre to avoid movement of the number portion when
// going negative.
X = -35,
Font = OsuFont.GetFont(size: 25, fixedWidth: true),
}
};
}
@ -34,7 +36,8 @@ namespace osu.Game.Screens.Edit.Components
{
base.Update();
trackTimer.Text = TimeSpan.FromMilliseconds(editorClock.CurrentTime).ToString(@"mm\:ss\:fff");
var timespan = TimeSpan.FromMilliseconds(editorClock.CurrentTime);
trackTimer.Text = $"{(timespan < TimeSpan.Zero ? "-" : string.Empty)}{timespan:mm\\:ss\\:fff}";
}
}
}

View File

@ -20,6 +20,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
@ -44,7 +45,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected EditorBeatmap EditorBeatmap { get; private set; }
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
protected IEditorChangeHandler ChangeHandler { get; private set; }
public SelectionHandler()
{
@ -193,12 +194,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void deleteSelected()
{
changeHandler?.BeginChange();
ChangeHandler?.BeginChange();
foreach (var h in selectedBlueprints.ToList())
EditorBeatmap?.Remove(h.HitObject);
changeHandler?.EndChange();
ChangeHandler?.EndChange();
}
#endregion
@ -254,7 +255,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="sampleName">The name of the hit sample.</param>
public void AddHitSample(string sampleName)
{
changeHandler?.BeginChange();
ChangeHandler?.BeginChange();
foreach (var h in SelectedHitObjects)
{
@ -265,7 +266,30 @@ namespace osu.Game.Screens.Edit.Compose.Components
h.Samples.Add(new HitSampleInfo { Name = sampleName });
}
changeHandler?.EndChange();
ChangeHandler?.EndChange();
}
/// <summary>
/// Set the new combo state of all selected <see cref="HitObject"/>s.
/// </summary>
/// <param name="state">Whether to set or unset.</param>
/// <exception cref="InvalidOperationException">Throws if any selected object doesn't implement <see cref="IHasComboInformation"/></exception>
public void SetNewCombo(bool state)
{
ChangeHandler?.BeginChange();
foreach (var h in SelectedHitObjects)
{
var comboInfo = h as IHasComboInformation;
if (comboInfo == null)
throw new InvalidOperationException($"Tried to change combo state of a {h.GetType()}, which doesn't implement {nameof(IHasComboInformation)}");
comboInfo.NewCombo = state;
EditorBeatmap?.UpdateHitObject(h);
}
ChangeHandler?.EndChange();
}
/// <summary>
@ -274,12 +298,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="sampleName">The name of the hit sample.</param>
public void RemoveHitSample(string sampleName)
{
changeHandler?.BeginChange();
ChangeHandler?.BeginChange();
foreach (var h in SelectedHitObjects)
h.SamplesBindable.RemoveAll(s => s.Name == sampleName);
changeHandler?.EndChange();
ChangeHandler?.EndChange();
}
#endregion
@ -297,6 +321,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
items.AddRange(GetContextMenuItemsForSelection(selectedBlueprints));
if (selectedBlueprints.All(b => b.HitObject is IHasComboInformation))
items.Add(createNewComboMenuItem());
if (selectedBlueprints.Count == 1)
items.AddRange(selectedBlueprints[0].ContextMenuItems);
@ -326,6 +353,41 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected virtual IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint> selection)
=> Enumerable.Empty<MenuItem>();
private MenuItem createNewComboMenuItem()
{
return new TernaryStateMenuItem("New combo", MenuItemType.Standard, setNewComboState)
{
State = { Value = getHitSampleState() }
};
void setNewComboState(TernaryState state)
{
switch (state)
{
case TernaryState.False:
SetNewCombo(false);
break;
case TernaryState.True:
SetNewCombo(true);
break;
}
}
TernaryState getHitSampleState()
{
int countExisting = selectedBlueprints.Select(b => (IHasComboInformation)b.HitObject).Count(h => h.NewCombo);
if (countExisting == 0)
return TernaryState.False;
if (countExisting < SelectedHitObjects.Count())
return TernaryState.Indeterminate;
return TernaryState.True;
}
}
private MenuItem createHitSampleMenuItem(string name, string sampleName)
{
return new TernaryStateMenuItem(name, MenuItemType.Standard, setHitSampleState)

View File

@ -79,6 +79,8 @@ namespace osu.Game.Screens.Edit
private EditorBeatmap editorBeatmap;
private EditorChangeHandler changeHandler;
private EditorMenuBar menuBar;
private DependencyContainer dependencies;
protected override UserActivity InitialActivity => new UserActivity.Editing(Beatmap.Value.BeatmapInfo);
@ -133,8 +135,6 @@ namespace osu.Game.Screens.Edit
updateLastSavedHash();
EditorMenuBar menuBar;
OsuMenuItem undoMenuItem;
OsuMenuItem redoMenuItem;
@ -374,14 +374,32 @@ namespace osu.Game.Screens.Edit
public bool OnPressed(GlobalAction action)
{
if (action == GlobalAction.Back)
switch (action)
{
// as we don't want to display the back button, manual handling of exit action is required.
this.Exit();
return true;
}
case GlobalAction.Back:
// as we don't want to display the back button, manual handling of exit action is required.
this.Exit();
return true;
return false;
case GlobalAction.EditorComposeMode:
menuBar.Mode.Value = EditorScreenMode.Compose;
return true;
case GlobalAction.EditorDesignMode:
menuBar.Mode.Value = EditorScreenMode.Design;
return true;
case GlobalAction.EditorTimingMode:
menuBar.Mode.Value = EditorScreenMode.Timing;
return true;
case GlobalAction.EditorSetupMode:
menuBar.Mode.Value = EditorScreenMode.SongSetup;
return true;
default:
return false;
}
}
public void OnReleased(GlobalAction action)

View File

@ -94,6 +94,7 @@ namespace osu.Game.Screens.Edit
}
},
};
LoadComponentAsync(CreateMainContent(), content =>
{
spinner.State.Value = Visibility.Hidden;

View File

@ -89,6 +89,8 @@ namespace osu.Game.Screens.Play
private void applyToPlayfield(Playfield playfield)
{
double failTime = playfield.Time.Current;
foreach (var nested in playfield.NestedPlayfields)
applyToPlayfield(nested);
@ -97,13 +99,29 @@ namespace osu.Game.Screens.Play
if (appliedObjects.Contains(obj))
continue;
obj.RotateTo(RNG.NextSingle(-90, 90), duration);
obj.ScaleTo(obj.Scale * 0.5f, duration);
obj.MoveToOffset(new Vector2(0, 400), duration);
float rotation = RNG.NextSingle(-90, 90);
Vector2 originalPosition = obj.Position;
Vector2 originalScale = obj.Scale;
dropOffScreen(obj, failTime, rotation, originalScale, originalPosition);
// need to reapply the fail drop after judgement state changes
obj.ApplyCustomUpdateState += (o, _) => dropOffScreen(obj, failTime, rotation, originalScale, originalPosition);
appliedObjects.Add(obj);
}
}
private void dropOffScreen(DrawableHitObject obj, double failTime, float randomRotation, Vector2 originalScale, Vector2 originalPosition)
{
using (obj.BeginAbsoluteSequence(failTime))
{
obj.RotateTo(randomRotation, duration);
obj.ScaleTo(originalScale * 0.5f, duration);
obj.MoveTo(originalPosition + new Vector2(0, 400), duration);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);

View File

@ -399,7 +399,7 @@ namespace osu.Game.Screens.Play
}
catch (Exception e)
{
Logger.Error(e, "Could not load beatmap sucessfully!");
Logger.Error(e, "Could not load beatmap successfully!");
//couldn't load, hard abort!
return null;
}

View File

@ -273,10 +273,10 @@ namespace osu.Game.Screens.Ranking
detachedPanelContainer.Add(expandedPanel);
// Move into its original location in the local container first, then to the final location.
var origLocation = detachedPanelContainer.ToLocalSpace(screenSpacePos);
expandedPanel.MoveTo(origLocation)
var origLocation = detachedPanelContainer.ToLocalSpace(screenSpacePos).X;
expandedPanel.MoveToX(origLocation)
.Then()
.MoveTo(new Vector2(StatisticsPanel.SIDE_PADDING, origLocation.Y), 150, Easing.OutQuint);
.MoveToX(StatisticsPanel.SIDE_PADDING, 150, Easing.OutQuint);
// Hide contracted panels.
foreach (var contracted in ScorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted))

View File

@ -99,6 +99,8 @@ namespace osu.Game.Screens.Ranking
{
var panel = new ScorePanel(score)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
PostExpandAction = () => PostExpandAction?.Invoke()
}.With(p =>
{

View File

@ -75,7 +75,23 @@ namespace osu.Game.Screens.Ranking.Statistics
return;
if (newScore.HitEvents == null || newScore.HitEvents.Count == 0)
content.Add(new MessagePlaceholder("Score has no statistics :("));
{
content.Add(new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new MessagePlaceholder("Extended statistics are only available after watching a replay!"),
new ReplayDownloadButton(newScore)
{
Scale = new Vector2(1.5f),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
}
});
}
else
{
spinner.Show();
@ -91,7 +107,10 @@ namespace osu.Game.Screens.Ranking.Statistics
{
var rows = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Vertical,
Spacing = new Vector2(30, 15),
Alpha = 0

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Audio;
@ -13,25 +14,62 @@ namespace osu.Game.Skinning
/// </summary>
public class BeatmapSkinProvidingContainer : SkinProvidingContainer
{
private readonly Bindable<bool> beatmapSkins = new Bindable<bool>();
private readonly Bindable<bool> beatmapHitsounds = new Bindable<bool>();
private Bindable<bool> beatmapSkins;
private Bindable<bool> beatmapHitsounds;
protected override bool AllowConfigurationLookup => beatmapSkins.Value;
protected override bool AllowDrawableLookup(ISkinComponent component) => beatmapSkins.Value;
protected override bool AllowTextureLookup(string componentName) => beatmapSkins.Value;
protected override bool AllowSampleLookup(ISampleInfo componentName) => beatmapHitsounds.Value;
protected override bool AllowConfigurationLookup
{
get
{
if (beatmapSkins == null)
throw new InvalidOperationException($"{nameof(BeatmapSkinProvidingContainer)} needs to be loaded before being consumed.");
return beatmapSkins.Value;
}
}
protected override bool AllowDrawableLookup(ISkinComponent component)
{
if (beatmapSkins == null)
throw new InvalidOperationException($"{nameof(BeatmapSkinProvidingContainer)} needs to be loaded before being consumed.");
return beatmapSkins.Value;
}
protected override bool AllowTextureLookup(string componentName)
{
if (beatmapSkins == null)
throw new InvalidOperationException($"{nameof(BeatmapSkinProvidingContainer)} needs to be loaded before being consumed.");
return beatmapSkins.Value;
}
protected override bool AllowSampleLookup(ISampleInfo componentName)
{
if (beatmapSkins == null)
throw new InvalidOperationException($"{nameof(BeatmapSkinProvidingContainer)} needs to be loaded before being consumed.");
return beatmapHitsounds.Value;
}
public BeatmapSkinProvidingContainer(ISkin skin)
: base(skin)
{
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins);
config.BindWith(OsuSetting.BeatmapHitsounds, beatmapHitsounds);
var config = parent.Get<OsuConfigManager>();
beatmapSkins = config.GetBindable<bool>(OsuSetting.BeatmapSkins);
beatmapHitsounds = config.GetBindable<bool>(OsuSetting.BeatmapHitsounds);
return base.CreateChildDependencies(parent);
}
[BackgroundDependencyLoader]
private void load()
{
beatmapSkins.BindValueChanged(_ => TriggerSourceChanged());
beatmapHitsounds.BindValueChanged(_ => TriggerSourceChanged());
}

View File

@ -0,0 +1,51 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Skinning
{
/// <summary>
/// An integer <see cref="RollingCounter{T}"/> that uses number sprites from a legacy skin.
/// </summary>
public class LegacyRollingCounter : RollingCounter<int>
{
private readonly ISkin skin;
private readonly string fontName;
private readonly float fontOverlap;
protected override bool IsRollingProportional => true;
/// <summary>
/// Creates a new <see cref="LegacyRollingCounter"/>.
/// </summary>
/// <param name="skin">The <see cref="ISkin"/> from which to get counter number sprites.</param>
/// <param name="fontName">The name of the legacy font to use.</param>
/// <param name="fontOverlap">
/// The numeric overlap of number sprites to use.
/// A positive number will bring the number sprites closer together, while a negative number
/// will split them apart more.
/// </param>
public LegacyRollingCounter(ISkin skin, string fontName, float fontOverlap)
{
this.skin = skin;
this.fontName = fontName;
this.fontOverlap = fontOverlap;
}
protected override double GetProportionalDuration(int currentValue, int newValue)
{
return Math.Abs(newValue - currentValue) * 75.0;
}
protected sealed override OsuSpriteText CreateSpriteText() =>
new LegacySpriteText(skin, fontName)
{
Spacing = new Vector2(-fontOverlap, 0f)
};
}
}

View File

@ -15,6 +15,8 @@ namespace osu.Game.Skinning
public enum LegacySetting
{
Version,
ComboPrefix,
ComboOverlap,
AnimationFramerate,
LayeredHitSounds,
}

View File

@ -62,6 +62,9 @@ namespace osu.Game.Skinning
}
}
public static bool HasFont(this ISkin source, string fontPrefix)
=> source.GetTexture($"{fontPrefix}-0") != null;
public class SkinnableTextureAnimation : TextureAnimation
{
[Resolved(canBeNull: true)]

View File

@ -114,6 +114,8 @@ namespace osu.Game.Skinning
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
bool wasPlaying = IsPlaying;
var channels = hitSamples.Select(s =>
{
var ch = skin.GetSample(s);
@ -138,8 +140,9 @@ namespace osu.Game.Skinning
samplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c));
if (requestedPlaying)
Play();
// Start playback internally for the new samples if the previous ones were playing beforehand.
if (wasPlaying)
play();
}
#region Re-expose AudioContainer

View File

@ -160,6 +160,11 @@ namespace osu.Game.Tests.Visual
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
var lookup = base.GetTexture(componentName, wrapModeS, wrapModeT);
if (lookup != null)
return lookup;
// extrapolate frames to test longer animations
if (extrapolateAnimations)
{
@ -169,7 +174,7 @@ namespace osu.Game.Tests.Visual
return base.GetTexture(componentName.Replace($"-{number}", $"-{number % 2}"), wrapModeS, wrapModeT);
}
return base.GetTexture(componentName, wrapModeS, wrapModeT);
return null;
}
}
}

View File

@ -24,7 +24,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.911.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.923.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
<PackageReference Include="Sentry" Version="2.1.6" />
<PackageReference Include="SharpCompress" Version="0.26.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.911.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.923.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
</ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
@ -80,7 +80,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.911.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.923.1" />
<PackageReference Include="SharpCompress" Version="0.26.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />