1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-25 18:57:18 +08:00

Merge branch 'master' into bss/the-actual-submission

This commit is contained in:
Bartłomiej Dach 2025-02-13 08:45:23 +01:00
commit de9362dae5
No known key found for this signature in database
45 changed files with 1098 additions and 452 deletions

View File

@ -96,7 +96,7 @@ jobs:
build-only-android:
name: Build only (Android)
runs-on: windows-2019
runs-on: windows-latest
timeout-minutes: 60
steps:
- name: Checkout
@ -114,10 +114,7 @@ jobs:
dotnet-version: "8.0.x"
- name: Install .NET workloads
# since windows image 20241113.3.0, not specifying a version here
# installs the .NET 7 version of android workload for very unknown reasons.
# revisit once we upgrade to .NET 9, it's probably fixed there.
run: dotnet workload install android --version (dotnet --version)
run: dotnet workload install android
- name: Compile
run: dotnet build -c Debug osu.Android.slnf

View File

@ -3,6 +3,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -12,6 +13,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
using osuTK.Graphics;
@ -26,18 +28,22 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
[Resolved]
private OsuColour colours { get; set; } = null!;
private IBindable<ScrollingDirection> direction = null!;
public ArgonJudgementPiece(HitResult result)
: base(result)
{
AutoSizeAxes = Axes.Both;
Origin = Anchor.Centre;
Y = judgement_y_position;
}
[BackgroundDependencyLoader]
private void load()
private void load(IScrollingInfo scrollingInfo)
{
direction = scrollingInfo.Direction.GetBoundCopy();
direction.BindValueChanged(_ => onDirectionChanged(), true);
if (Result.IsHit())
{
AddInternal(ringExplosion = new RingExplosion(Result)
@ -47,6 +53,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
}
}
private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
protected override SpriteText CreateJudgementText() =>
new OsuSpriteText
{
@ -78,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
this.MoveToY(judgement_y_position);
this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position);
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
this.RotateTo(0);

View File

@ -2,12 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
@ -28,14 +30,16 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
float hitPosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0;
float scorePosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0;
private IBindable<ScrollingDirection> direction = null!;
float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition;
Y = scorePosition - absoluteHitPosition;
[Resolved]
private ISkinSource skin { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
direction = scrollingInfo.Direction.GetBoundCopy();
direction.BindValueChanged(_ => onDirectionChanged(), true);
InternalChild = animation.With(d =>
{
@ -44,6 +48,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
});
}
private void onDirectionChanged()
{
float hitPosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0;
float scorePosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0;
float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition;
float finalPosition = scorePosition - absoluteHitPosition;
Y = direction.Value == ScrollingDirection.Up ? -finalPosition : finalPosition;
}
public void PlayAnimation()
{
(animation as IFramedAnimation)?.GotoFrame(0);

View File

@ -0,0 +1,75 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
public partial class DefaultManiaJudgementPiece : DefaultJudgementPiece
{
private const float judgement_y_position = -180f;
private IBindable<ScrollingDirection> direction = null!;
public DefaultManiaJudgementPiece(HitResult result)
: base(result)
{
}
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
direction = scrollingInfo.Direction.GetBoundCopy();
direction.BindValueChanged(_ => onDirectionChanged(), true);
}
private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
protected override void LoadComplete()
{
base.LoadComplete();
JudgementText.Font = JudgementText.Font.With(size: 25);
}
public override void PlayAnimation()
{
switch (Result)
{
case HitResult.None:
this.FadeOutFromOne(800);
break;
case HitResult.Miss:
this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position);
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
this.RotateTo(0);
this.RotateTo(40, 800, Easing.InQuint);
this.FadeOutFromOne(800);
break;
default:
this.ScaleTo(0.8f);
this.ScaleTo(1, 250, Easing.OutElastic);
this.Delay(50)
.ScaleTo(0.75f, 250)
.FadeOut(200);
// osu!mania uses a custom fade length, so the base call is intentionally omitted.
break;
}
}
}
}

View File

@ -3,65 +3,32 @@
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osuTK;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.UI
{
public partial class DrawableManiaJudgement : DrawableJudgement
{
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
private IBindable<ScrollingDirection> direction;
private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
private const float judgement_y_position = -180f;
public DefaultManiaJudgementPiece(HitResult result)
: base(result)
{
Y = judgement_y_position;
}
protected override void LoadComplete()
{
base.LoadComplete();
JudgementText.Font = JudgementText.Font.With(size: 25);
}
public override void PlayAnimation()
{
switch (Result)
{
case HitResult.None:
this.FadeOutFromOne(800);
break;
case HitResult.Miss:
this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
this.MoveToY(judgement_y_position);
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
this.RotateTo(0);
this.RotateTo(40, 800, Easing.InQuint);
this.FadeOutFromOne(800);
break;
default:
this.ScaleTo(0.8f);
this.ScaleTo(1, 250, Easing.OutElastic);
this.Delay(50)
.ScaleTo(0.75f, 250)
.FadeOut(200);
break;
}
}
direction = scrollingInfo.Direction.GetBoundCopy();
direction.BindValueChanged(_ => onDirectionChanged(), true);
}
private void onDirectionChanged()
{
Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
Origin = Anchor.Centre;
}
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
}
}

View File

@ -216,13 +216,7 @@ namespace osu.Game.Rulesets.Mania.UI
return;
judgements.Clear(false);
judgements.Add(judgementPooler.Get(result.Type, j =>
{
j.Apply(result, judgedObject);
j.Anchor = Anchor.BottomCentre;
j.Origin = Anchor.Centre;
})!);
judgements.Add(judgementPooler.Get(result.Type, j => j.Apply(result, judgedObject))!);
}
protected override void Update()

View File

@ -66,8 +66,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Slider slider = drawableSlider.HitObject;
Position = slider.CurvePositionAt(completionProgress);
//0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1
var diff = slider.CurvePositionAt(completionProgress) - slider.CurvePositionAt(Math.Min(1, completionProgress + 0.1 / slider.Path.Distance));
// 0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1
double checkDistance = 0.1 / slider.Path.Distance;
var diff = slider.CurvePositionAt(Math.Min(1 - checkDistance, completionProgress)) - slider.CurvePositionAt(Math.Min(1, completionProgress + checkDistance));
// Ensure the value is substantially high enough to allow for Atan2 to get a valid angle.
// Needed for when near completion, or in case of a very short slider.

View File

@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
@ -13,8 +14,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
public abstract partial class FollowCircle : CompositeDrawable
{
[Resolved]
protected DrawableHitObject? ParentObject { get; private set; }
protected DrawableSlider? DrawableObject { get; private set; }
private readonly IBindable<bool> tracking = new Bindable<bool>();
protected FollowCircle()
{
@ -22,65 +24,73 @@ namespace osu.Game.Rulesets.Osu.Skinning
}
[BackgroundDependencyLoader]
private void load()
private void load(DrawableHitObject? hitObject)
{
((DrawableSlider?)ParentObject)?.Tracking.BindValueChanged(tracking =>
DrawableObject = hitObject as DrawableSlider;
if (DrawableObject != null)
{
Debug.Assert(ParentObject != null);
if (ParentObject.Judged)
return;
using (BeginAbsoluteSequence(Math.Max(Time.Current, ParentObject.HitObject?.StartTime ?? 0)))
tracking.BindTo(DrawableObject.Tracking);
tracking.BindValueChanged(tracking =>
{
if (tracking.NewValue)
OnSliderPress();
else
OnSliderRelease();
}
}, true);
if (DrawableObject.Judged)
return;
using (BeginAbsoluteSequence(Math.Max(Time.Current, DrawableObject.HitObject?.StartTime ?? 0)))
{
if (tracking.NewValue)
OnSliderPress();
else
OnSliderRelease();
}
}, true);
}
}
protected override void LoadComplete()
{
base.LoadComplete();
if (ParentObject != null)
if (DrawableObject != null)
{
ParentObject.HitObjectApplied += onHitObjectApplied;
onHitObjectApplied(ParentObject);
DrawableObject.HitObjectApplied += onHitObjectApplied;
onHitObjectApplied(DrawableObject);
ParentObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(ParentObject, ParentObject.State.Value);
DrawableObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(DrawableObject, DrawableObject.State.Value);
}
}
private void onHitObjectApplied(DrawableHitObject drawableObject)
{
// Sane defaults when a new hitobject is applied to the drawable slider.
this.ScaleTo(1f)
.FadeOut();
// Immediately play out any pending transforms from press/release
FinishTransforms(true);
}
private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state)
private void updateStateTransforms(DrawableHitObject d, ArmedState state)
{
Debug.Assert(ParentObject != null);
Debug.Assert(DrawableObject != null);
switch (state)
{
case ArmedState.Hit:
switch (drawableObject)
switch (d)
{
case DrawableSliderTail:
// Use ParentObject instead of drawableObject because slider tail's
// Use DrawableObject instead of local object because slider tail's
// HitStateUpdateTime is ~36ms before the actual slider end (aka slider
// tail leniency)
using (BeginAbsoluteSequence(ParentObject.HitStateUpdateTime))
using (BeginAbsoluteSequence(DrawableObject.HitStateUpdateTime))
OnSliderEnd();
break;
case DrawableSliderTick:
case DrawableSliderRepeat:
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
using (BeginAbsoluteSequence(d.HitStateUpdateTime))
OnSliderTick();
break;
}
@ -88,15 +98,15 @@ namespace osu.Game.Rulesets.Osu.Skinning
break;
case ArmedState.Miss:
switch (drawableObject)
switch (d)
{
case DrawableSliderTail:
case DrawableSliderTick:
case DrawableSliderRepeat:
// Despite above comment, ok to use drawableObject.HitStateUpdateTime
// Despite above comment, ok to use d.HitStateUpdateTime
// here, since on stable, the break anim plays right when the tail is
// missed, not when the slider ends
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
using (BeginAbsoluteSequence(d.HitStateUpdateTime))
OnSliderBreak();
break;
}
@ -109,10 +119,10 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
base.Dispose(isDisposing);
if (ParentObject != null)
if (DrawableObject != null)
{
ParentObject.HitObjectApplied -= onHitObjectApplied;
ParentObject.ApplyCustomUpdateState -= updateStateTransforms;
DrawableObject.HitObjectApplied -= onHitObjectApplied;
DrawableObject.ApplyCustomUpdateState -= updateStateTransforms;
}
}

View File

@ -22,9 +22,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
protected override void OnSliderPress()
{
Debug.Assert(ParentObject != null);
Debug.Assert(DrawableObject != null);
double remainingTime = Math.Max(0, ParentObject.HitStateUpdateTime - Time.Current);
double remainingTime = Math.Max(0, DrawableObject.HitStateUpdateTime - Time.Current);
// Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour.
// This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this).

View File

@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new osuTK.Vector2(0.5f),
}));
}

View File

@ -6,14 +6,9 @@
using System;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Skinning.Default;
@ -25,11 +20,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public partial class DrawableSwell : DrawableTaikoHitObject<Swell>
{
private const float target_ring_thick_border = 1.4f;
private const float target_ring_thin_border = 1f;
private const float target_ring_scale = 5f;
private const float inner_ring_alpha = 0.65f;
/// <summary>
/// Offset away from the start time of the swell at which the ring starts appearing.
/// </summary>
@ -38,9 +28,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private Vector2 baseSize;
private readonly Container<DrawableSwellTick> ticks;
private readonly Container bodyContainer;
private readonly CircularContainer targetRing;
private readonly CircularContainer expandingRing;
private double? lastPressHandleTime;
@ -51,6 +38,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
/// </summary>
public bool MustAlternate { get; internal set; } = true;
public event Action<int> UpdateHitProgress;
public DrawableSwell()
: this(null)
{
@ -61,87 +50,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
FillMode = FillMode.Fit;
Content.Add(bodyContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Depth = 1,
Children = new Drawable[]
{
expandingRing = new CircularContainer
{
Name = "Expanding ring",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0,
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Masking = true,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = inner_ring_alpha,
}
}
},
targetRing = new CircularContainer
{
Name = "Target ring (thick border)",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = target_ring_thick_border,
Blending = BlendingParameters.Additive,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
},
new CircularContainer
{
Name = "Target ring (thin border)",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = target_ring_thin_border,
BorderColour = Color4.White,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
}
}
}
}
});
AddInternal(ticks = new Container<DrawableSwellTick> { RelativeSizeAxes = Axes.Both });
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
expandingRing.Colour = colours.YellowLight;
targetRing.BorderColour = colours.YellowDark.Opacity(0.25f);
}
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell),
_ => new SwellCirclePiece
_ => new DefaultSwell
{
// to allow for rotation transform
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
});
protected override void RecreatePieces()
@ -208,16 +125,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
int numHits = ticks.Count(r => r.IsHit);
float completion = (float)numHits / HitObject.RequiredHits;
expandingRing
.FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50)
.Then()
.FadeTo(completion / 8, 2000, 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);
UpdateHitProgress?.Invoke(numHits);
if (numHits == HitObject.RequiredHits)
ApplyMaxResult();
@ -248,28 +156,21 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
}
}
protected override void UpdateStartTimeStateTransforms()
{
base.UpdateStartTimeStateTransforms();
using (BeginDelayedSequence(-ring_appear_offset))
targetRing.ScaleTo(target_ring_scale, 400, Easing.OutQuint);
}
protected override void UpdateHitStateTransforms(ArmedState state)
{
const double transition_duration = 300;
base.UpdateHitStateTransforms(state);
switch (state)
{
case ArmedState.Idle:
expandingRing.FadeTo(0);
break;
case ArmedState.Miss:
this.Delay(300).FadeOut();
break;
case ArmedState.Hit:
this.FadeOut(transition_duration, Easing.Out);
bodyContainer.ScaleTo(1.4f, transition_duration);
this.Delay(660).FadeOut();
break;
}
}

View File

@ -0,0 +1,20 @@
// 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.Game.Rulesets.Taiko.Skinning.Default;
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{
public partial class ArgonSwell : DefaultSwell
{
protected override Drawable CreateCentreCircle()
{
return new ArgonSwellCirclePiece
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
}
}
}

View File

@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
return new ArgonHitExplosion(taikoComponent.Component);
case TaikoSkinComponents.Swell:
return new ArgonSwellCirclePiece();
return new ArgonSwell();
}
break;

View File

@ -0,0 +1,190 @@
// 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.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.Skinning.Default
{
public partial class DefaultSwell : Container
{
private const float target_ring_thick_border = 1.4f;
private const float target_ring_thin_border = 1f;
private const float target_ring_scale = 5f;
private const float inner_ring_alpha = 0.65f;
private DrawableSwell drawableSwell = null!;
private readonly Container bodyContainer;
private readonly CircularContainer targetRing;
private readonly CircularContainer expandingRing;
private readonly Drawable centreCircle;
private int numHits;
public DefaultSwell()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
Content.Add(bodyContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Depth = 1,
Children = new[]
{
expandingRing = new CircularContainer
{
Name = "Expanding ring",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0,
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Masking = true,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = inner_ring_alpha,
}
}
},
targetRing = new CircularContainer
{
Name = "Target ring (thick border)",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = target_ring_thick_border,
Blending = BlendingParameters.Additive,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
},
new CircularContainer
{
Name = "Target ring (thin border)",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = target_ring_thin_border,
BorderColour = Color4.White,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
}
}
},
centreCircle = CreateCentreCircle(),
}
});
}
[BackgroundDependencyLoader]
private void load(DrawableHitObject hitObject, OsuColour colours)
{
drawableSwell = (DrawableSwell)hitObject;
drawableSwell.UpdateHitProgress += animateSwellProgress;
drawableSwell.ApplyCustomUpdateState += updateStateTransforms;
expandingRing.Colour = colours.YellowLight;
targetRing.BorderColour = colours.YellowDark.Opacity(0.25f);
}
protected virtual Drawable CreateCentreCircle()
{
return new SwellCirclePiece
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
}
private void animateSwellProgress(int numHits)
{
this.numHits = numHits;
float completion = (float)numHits / drawableSwell.HitObject.RequiredHits;
expandingRing.Alpha += Math.Clamp(completion / 16, 0.1f, 0.6f);
}
protected override void Update()
{
base.Update();
float completion = (float)numHits / drawableSwell.HitObject.RequiredHits;
centreCircle.Rotation = (float)Interpolation.DampContinuously(centreCircle.Rotation,
(float)(completion * drawableSwell.HitObject.Duration / 8), 500, Math.Abs(Time.Elapsed));
expandingRing.Scale = new Vector2((float)Interpolation.DampContinuously(expandingRing.Scale.X,
1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 35, Math.Abs(Time.Elapsed)));
expandingRing.Alpha = (float)Interpolation.DampContinuously(expandingRing.Alpha, completion / 16, 250, Math.Abs(Time.Elapsed));
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
if (!(drawableHitObject is DrawableSwell))
return;
Swell swell = drawableSwell.HitObject;
using (BeginAbsoluteSequence(swell.StartTime))
{
if (state == ArmedState.Idle)
expandingRing.FadeTo(0);
const double ring_appear_offset = 100;
targetRing.Delay(ring_appear_offset).ScaleTo(target_ring_scale, 400, Easing.OutQuint);
}
using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime))
{
const double transition_duration = 300;
bodyContainer.FadeOut(transition_duration, Easing.OutQuad);
bodyContainer.ScaleTo(1.4f, transition_duration);
centreCircle.FadeOut(transition_duration, Easing.OutQuad);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableSwell.IsNotNull())
{
drawableSwell.UpdateHitProgress -= animateSwellProgress;
drawableSwell.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}
}

View File

@ -0,0 +1,201 @@
// 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.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Game.Skinning;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Audio;
using osuTK;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Extensions.ObjectExtensions;
using System;
using System.Globalization;
using osu.Framework.Utils;
namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
public partial class LegacySwell : Container
{
private const float scale_adjust = 768f / 480;
private static readonly Vector2 swell_display_position = new Vector2(250f, 100f);
private DrawableSwell drawableSwell = null!;
private Container bodyContainer = null!;
private Sprite warning = null!;
private Sprite spinnerCircle = null!;
private Sprite approachCircle = null!;
private Sprite clearAnimation = null!;
private SkinnableSound clearSample = null!;
private LegacySpriteText remainingHitsText = null!;
private bool samplePlayed;
private int numHits;
public LegacySwell()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(DrawableHitObject hitObject, ISkinSource skin)
{
Children = new Drawable[]
{
warning = new Sprite
{
Texture = skin.GetTexture("spinner-warning"),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f),
},
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Position = swell_display_position, // ballparked to be horizontally centred on 4:3 resolution
Children = new Drawable[]
{
bodyContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0,
Children = new Drawable[]
{
spinnerCircle = new Sprite
{
Texture = skin.GetTexture("spinner-circle"),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(0.8f),
},
approachCircle = new Sprite
{
Texture = skin.GetTexture("spinner-approachcircle"),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(1.86f * 0.8f),
Alpha = 0.8f,
},
remainingHitsText = new LegacySpriteText(LegacyFont.Score)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Position = new Vector2(0f, 130f),
Scale = Vector2.One,
},
}
},
clearAnimation = new Sprite
{
Texture = skin.GetTexture("spinner-osu"),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0,
Y = -40,
},
},
},
clearSample = new SkinnableSound(new SampleInfo("spinner-osu")),
};
drawableSwell = (DrawableSwell)hitObject;
drawableSwell.UpdateHitProgress += animateSwellProgress;
drawableSwell.ApplyCustomUpdateState += updateStateTransforms;
}
private void animateSwellProgress(int numHits)
{
this.numHits = numHits;
remainingHitsText.Text = (drawableSwell.HitObject.RequiredHits - numHits).ToString(CultureInfo.InvariantCulture);
spinnerCircle.Scale = new Vector2(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f));
}
protected override void Update()
{
base.Update();
int requiredHits = drawableSwell.HitObject.RequiredHits;
int remainingHits = requiredHits - numHits;
remainingHitsText.Scale = new Vector2((float)Interpolation.DampContinuously(
remainingHitsText.Scale.X, 1.6f - (0.6f * ((float)remainingHits / requiredHits)), 17.5, Math.Abs(Time.Elapsed)));
spinnerCircle.Rotation = (float)Interpolation.DampContinuously(spinnerCircle.Rotation, 180f * numHits, 130, Math.Abs(Time.Elapsed));
spinnerCircle.Scale = new Vector2((float)Interpolation.DampContinuously(
spinnerCircle.Scale.X, 0.8f, 120, Math.Abs(Time.Elapsed)));
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
if (!(drawableHitObject is DrawableSwell))
return;
Swell swell = drawableSwell.HitObject;
using (BeginAbsoluteSequence(swell.StartTime))
{
if (state == ArmedState.Idle)
{
remainingHitsText.Text = $"{swell.RequiredHits}";
samplePlayed = false;
}
const double body_transition_duration = 200;
warning.MoveTo(swell_display_position, body_transition_duration)
.ScaleTo(3, body_transition_duration, Easing.Out)
.FadeOut(body_transition_duration);
bodyContainer.FadeIn(body_transition_duration);
approachCircle.ResizeTo(0.1f * 0.8f, swell.Duration);
}
using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime))
{
const double clear_transition_duration = 300;
const double clear_fade_in = 120;
bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad);
spinnerCircle.ScaleTo(spinnerCircle.Scale.X + 0.05f, clear_transition_duration, Easing.OutQuad);
if (state == ArmedState.Hit)
{
if (!samplePlayed)
{
clearSample.Play();
samplePlayed = true;
}
clearAnimation
.MoveToOffset(new Vector2(0, -90 * scale_adjust), clear_fade_in * 2, Easing.Out)
.ScaleTo(0.4f)
.ScaleTo(1f, clear_fade_in * 2, Easing.Out)
.FadeIn()
.Delay(clear_fade_in * 3)
.FadeOut(clear_fade_in * 2.5);
}
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableSwell.IsNotNull())
{
drawableSwell.UpdateHitProgress -= animateSwellProgress;
drawableSwell.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}
}

View File

@ -66,7 +66,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
return this.GetAnimation("sliderscorepoint", false, false);
case TaikoSkinComponents.Swell:
// todo: support taiko legacy swell (https://github.com/ppy/osu/issues/13601).
if (GetTexture("spinner-circle") != null)
return new LegacySwell();
return null;
case TaikoSkinComponents.HitTarget:

View File

@ -6,6 +6,7 @@ using System.Linq;
using Moq;
using NUnit.Framework;
using osu.Framework.Localisation;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
@ -342,6 +343,40 @@ namespace osu.Game.Tests.Mods
Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.055).ToString(), "1.06x");
}
[Test]
public void TestRoomModValidity()
{
Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModHardRock(), MatchType.Playlists));
Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModDoubleTime(), MatchType.Playlists));
Assert.IsTrue(ModUtils.IsValidModForMatchType(new ModAdaptiveSpeed(), MatchType.Playlists));
Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModAutoplay(), MatchType.Playlists));
Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModTouchDevice(), MatchType.Playlists));
Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModHardRock(), MatchType.HeadToHead));
Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModDoubleTime(), MatchType.HeadToHead));
// For now, adaptive speed isn't allowed in multiplayer because it's a per-user rate adjustment.
Assert.IsFalse(ModUtils.IsValidModForMatchType(new ModAdaptiveSpeed(), MatchType.HeadToHead));
Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModAutoplay(), MatchType.HeadToHead));
Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModTouchDevice(), MatchType.HeadToHead));
}
[Test]
public void TestRoomFreeModValidity()
{
Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModHardRock(), MatchType.Playlists));
Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModDoubleTime(), MatchType.Playlists));
Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new ModAdaptiveSpeed(), MatchType.Playlists));
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModAutoplay(), MatchType.Playlists));
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModTouchDevice(), MatchType.Playlists));
Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModHardRock(), MatchType.HeadToHead));
// For now, all rate adjustment mods aren't allowed as free mods in multiplayer.
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModDoubleTime(), MatchType.HeadToHead));
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new ModAdaptiveSpeed(), MatchType.HeadToHead));
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModAutoplay(), MatchType.HeadToHead));
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModTouchDevice(), MatchType.HeadToHead));
}
public abstract class CustomMod1 : Mod, IModCompatibilitySpecification
{
}

View File

@ -8,11 +8,14 @@ using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual.Multiplayer;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Tests.Visual.Spectator;
namespace osu.Game.Tests.Visual.Gameplay
@ -28,20 +31,23 @@ namespace osu.Game.Tests.Visual.Gameplay
SpectatorList list = null!;
Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>();
GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState);
TestSpectatorClient client = new TestSpectatorClient();
TestSpectatorClient spectatorClient = new TestSpectatorClient();
TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestMultiplayerRoomManager(new TestRoomRequestsHandler()));
AddStep("create spectator list", () =>
{
Children = new Drawable[]
{
client,
spectatorClient,
multiplayerClient,
new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies =
[
(typeof(GameplayState), gameplayState),
(typeof(SpectatorClient), client)
(typeof(SpectatorClient), spectatorClient),
(typeof(MultiplayerClient), multiplayerClient),
],
Child = list = new SpectatorList
{
@ -57,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddRepeatStep("add a user", () =>
{
int id = Interlocked.Increment(ref counter);
((ISpectatorClient)client).UserStartedWatching([
((ISpectatorClient)spectatorClient).UserStartedWatching([
new SpectatorUser
{
OnlineID = id,
@ -66,7 +72,8 @@ namespace osu.Game.Tests.Visual.Gameplay
]);
}, 10);
AddRepeatStep("remove random user", () => ((ISpectatorClient)client).UserEndedWatching(client.WatchingUsers[RNG.Next(client.WatchingUsers.Count)].OnlineID), 5);
AddRepeatStep("remove random user", () => ((ISpectatorClient)spectatorClient).UserEndedWatching(
spectatorClient.WatchingUsers[RNG.Next(spectatorClient.WatchingUsers.Count)].OnlineID), 5);
AddStep("change font to venera", () => list.Font.Value = Typeface.Venera);
AddStep("change font to torus", () => list.Font.Value = Typeface.Torus);

View File

@ -166,7 +166,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Y = -ScreenFooter.HEIGHT,
Current = { BindTarget = freeModSelectOverlay.SelectedMods },
FreeMods = { BindTarget = freeModSelectOverlay.SelectedMods },
},
footer = new ScreenFooter(),
},

View File

@ -12,11 +12,14 @@ using osu.Framework.Utils;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
using osu.Game.Users;
using osuTK;
@ -393,6 +396,40 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
}
[Test]
public void TestModsAndRuleset()
{
AddStep("add another user", () =>
{
MultiplayerClient.AddUser(new APIUser
{
Id = 0,
Username = "User 0",
RulesetsStatistics = new Dictionary<string, UserStatistics>
{
{
Ruleset.Value.ShortName,
new UserStatistics { GlobalRank = RNG.Next(1, 100000), }
}
},
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
});
MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable());
});
AddStep("set user styles", () =>
{
MultiplayerClient.ChangeUserStyle(API.LocalUser.Value.OnlineID, 259, 1);
MultiplayerClient.ChangeUserMods(API.LocalUser.Value.OnlineID,
[new APIMod(new TaikoModConstantSpeed()), new APIMod(new TaikoModHidden()), new APIMod(new TaikoModFlashlight()), new APIMod(new TaikoModHardRock())]);
MultiplayerClient.ChangeUserStyle(0, 259, 2);
MultiplayerClient.ChangeUserMods(0,
[new APIMod(new CatchModFloatingFruits()), new APIMod(new CatchModHidden()), new APIMod(new CatchModMirror())]);
});
}
private void createNewParticipantsList()
{
ParticipantsList? participantsList = null;

View File

@ -266,7 +266,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void assertQueueTabCount(int count)
{
string queueTabText = count > 0 ? $"Queue ({count})" : "Queue";
string queueTabText = count > 0 ? $"Up next ({count})" : "Up next";
AddUntilStep($"Queue tab shows \"{queueTabText}\"", () =>
{
return this.ChildrenOfType<OsuTabControl<MultiplayerPlaylistDisplayMode>.OsuTabItem>()

View File

@ -30,8 +30,8 @@ namespace osu.Game.Beatmaps
{
Metadata = new BeatmapMetadata
{
Artist = "please load a beatmap!",
Title = "no beatmaps available!"
Artist = "please select or load a beatmap!",
Title = "no beatmap selected!"
},
BeatmapSet = new BeatmapSetInfo(),
Difficulty = new BeatmapDifficulty

View File

@ -96,7 +96,6 @@ namespace osu.Game.Collections
lastCreated = collections[changes.InsertedIndices[0]].ID;
foreach (int i in changes.NewModifiedIndices)
{
var updatedItem = collections[i];

View File

@ -15,6 +15,7 @@ using osu.Framework.Localisation;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
@ -27,7 +28,7 @@ namespace osu.Game.Collections
/// </summary>
public partial class DrawableCollectionListItem : OsuRearrangeableListItem<Live<BeatmapCollection>>, IFilterable
{
private const float item_height = 35;
private const float item_height = 45;
private const float button_width = item_height * 0.75f;
protected TextBox TextBox => content.TextBox;
@ -92,13 +93,11 @@ namespace osu.Game.Collections
Padding = new MarginPadding { Right = collection.IsManaged ? button_width : 0 },
Children = new Drawable[]
{
TextBox = new ItemTextBox
TextBox = new ItemTextBox(collection)
{
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
CornerRadius = item_height / 2,
RelativeSizeAxes = Axes.X,
Height = item_height,
CommitOnFocusLost = true,
PlaceholderText = collection.IsManaged ? string.Empty : "Create a new collection"
},
}
},
@ -125,11 +124,57 @@ namespace osu.Game.Collections
{
protected override float LeftRightPadding => item_height / 2;
private const float count_text_size = 12;
private readonly Live<BeatmapCollection> collection;
private OsuSpriteText countText = null!;
public ItemTextBox(Live<BeatmapCollection> collection)
{
this.collection = collection;
CornerRadius = item_height / 2;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
BackgroundUnfocused = colours.GreySeaFoamDarker.Darken(0.5f);
BackgroundFocused = colours.GreySeaFoam;
if (collection.IsManaged)
{
TextContainer.Height *= (Height - count_text_size) / Height;
TextContainer.Margin = new MarginPadding { Bottom = count_text_size };
TextContainer.Add(countText = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft,
Depth = float.MinValue,
Font = OsuFont.Default.With(size: count_text_size, weight: FontWeight.SemiBold),
Margin = new MarginPadding { Top = 2, Left = 2 },
Colour = colours.Yellow
});
// interestingly, it is not required to subscribe to change notifications on this collection at all for this to work correctly.
// the reasoning for this is that `DrawableCollectionList` already takes out a subscription on the set of all `BeatmapCollection`s -
// but that subscription does not only cover *changes to the set of collections* (i.e. addition/removal/rearrangement of collections),
// but also covers *changes to the properties of collections*, which `BeatmapMD5Hashes` is one.
// when a collection item changes due to `BeatmapMD5Hashes` changing, the list item is deleted and re-inserted, thus guaranteeing this to work correctly.
int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count);
countText.Text = count == 1
// Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918
// but also in this case we want support for formatting a number within a string).
? $"{count:#,0} item"
: $"{count:#,0} items";
}
else
{
PlaceholderText = "Create a new collection";
}
}
}
@ -210,7 +255,7 @@ namespace osu.Game.Collections
private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c));
}
public IEnumerable<LocalisableString> FilterTerms => [(LocalisableString)Model.Value.Name];
public IEnumerable<LocalisableString> FilterTerms => Model.PerformRead(m => m.IsValid ? new[] { (LocalisableString)m.Name } : []);
private bool matchingFilter = true;

View File

@ -227,6 +227,10 @@ namespace osu.Game.Input.Bindings
};
}
/// <remarks>
/// IMPORTANT: New entries should always be added at the end of the enum, as key bindings are stored using the enum's numeric value and
/// changes in order would cause key bindings to get associated with the wrong action.
/// </remarks>
public enum GlobalAction
{
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleChat))]

View File

@ -194,6 +194,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString ResetBookmarks => new TranslatableString(getKey(@"reset_bookmarks"), @"Reset bookmarks");
/// <summary>
/// "Open beatmap info page"
/// </summary>
public static LocalisableString OpenInfoPage => new TranslatableString(getKey(@"open_info_page"), @"Open beatmap info page");
/// <summary>
/// "Open beatmap discussion page"
/// </summary>
public static LocalisableString OpenDiscussionPage => new TranslatableString(getKey(@"open_discussion_page"), @"Open beatmap discussion page");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
}

View File

@ -18,6 +18,7 @@ namespace osu.Game.Screens.Edit.Components
public partial class TimeInfoContainer : BottomBarContainer
{
private OsuSpriteText bpm = null!;
private OsuSpriteText progress = null!;
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
@ -36,26 +37,44 @@ namespace osu.Game.Screens.Edit.Components
bpm = new OsuSpriteText
{
Colour = colours.Orange1,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold, fixedWidth: true),
Spacing = new Vector2(-1, 0),
Position = new Vector2(0, 4),
Anchor = Anchor.CentreRight,
Origin = Anchor.TopRight,
},
progress = new OsuSpriteText
{
Colour = colours.Purple1,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold, fixedWidth: true),
Spacing = new Vector2(-1, 0),
Anchor = Anchor.CentreLeft,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
Position = new Vector2(2, 4),
}
};
}
private double? lastBPM;
private double? lastProgress;
protected override void Update()
{
base.Update();
double newBPM = editorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime).BPM;
double newProgress = (int)(editorClock.CurrentTime / editorClock.TrackLength * 100);
if (lastBPM != newBPM)
{
lastBPM = newBPM;
bpm.Text = @$"{newBPM:0} BPM";
}
if (lastProgress != newProgress)
{
lastProgress = newProgress;
progress.Text = @$"{newProgress:0}%";
}
}
private partial class TimestampControl : OsuClickableContainer

View File

@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@ -14,6 +15,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
{
public partial class TestGameplayButton : OsuButton
{
[Resolved]
private OsuColour colours { get; set; } = null!;
protected override SpriteText CreateText() => new OsuSpriteText
{
Depth = -1,
@ -24,7 +28,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
};
[BackgroundDependencyLoader]
private void load(OsuColour colours, OverlayColourProvider colourProvider)
private void load(OverlayColourProvider colourProvider)
{
BackgroundColour = colours.Orange1;
SpriteText.Colour = colourProvider.Background6;
@ -33,5 +37,18 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
Text = EditorStrings.TestBeatmap;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
Background.FadeColour(colours.Orange0, 500, Easing.OutQuint);
// don't call base in order to block scale animation
return false;
}
protected override void OnMouseUp(MouseUpEvent e)
{
Background.FadeColour(colours.Orange1, 300, Easing.OutQuint);
// don't call base in order to block scale animation
}
}
}

View File

@ -1273,6 +1273,15 @@ namespace osu.Game.Screens.Edit
yield return upload;
}
if (editorBeatmap.BeatmapInfo.OnlineID > 0)
{
yield return new OsuMenuItemSpacer();
yield return new EditorMenuItem(EditorStrings.OpenInfoPage, MenuItemType.Standard,
() => (Game as OsuGame)?.OpenUrlExternally(editorBeatmap.BeatmapInfo.GetOnlineURL(api, editorBeatmap.BeatmapInfo.Ruleset)));
yield return new EditorMenuItem(EditorStrings.OpenDiscussionPage, MenuItemType.Standard,
() => (Game as OsuGame)?.OpenUrlExternally($@"{api.Endpoints.WebsiteUrl}/beatmapsets/{editorBeatmap.BeatmapInfo.BeatmapSet!.OnlineID}/discussion/{editorBeatmap.BeatmapInfo.OnlineID}"));
}
yield return new OsuMenuItemSpacer();
yield return new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit);
}

View File

@ -53,13 +53,11 @@ namespace osu.Game.Screens.OnlinePlay.Components
{
RelativeSizeAxes = Axes.X,
Height = 2,
Margin = new MarginPadding { Bottom = 2 }
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Top = 5 },
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{

View File

@ -11,31 +11,22 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
using osuTK;
using osu.Game.Localisation;
namespace osu.Game.Screens.OnlinePlay
{
public partial class FooterButtonFreeMods : FooterButton, IHasCurrentValue<IReadOnlyList<Mod>>
public partial class FooterButtonFreeMods : FooterButton
{
private readonly BindableWithCurrent<IReadOnlyList<Mod>> current = new BindableWithCurrent<IReadOnlyList<Mod>>(Array.Empty<Mod>());
public readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>();
public readonly IBindable<bool> Freestyle = new Bindable<bool>();
public Bindable<IReadOnlyList<Mod>> Current
{
get => current.Current;
set
{
ArgumentNullException.ThrowIfNull(value);
current.Current = value;
}
}
protected override bool IsActive => FreeMods.Value.Count > 0;
public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); }
@ -104,7 +95,8 @@ namespace osu.Game.Screens.OnlinePlay
{
base.LoadComplete();
Current.BindValueChanged(_ => updateModDisplay(), true);
Freestyle.BindValueChanged(_ => updateModDisplay());
FreeMods.BindValueChanged(_ => updateModDisplay(), true);
}
/// <summary>
@ -114,16 +106,16 @@ namespace osu.Game.Screens.OnlinePlay
{
var availableMods = allAvailableAndValidMods.ToArray();
Current.Value = Current.Value.Count == availableMods.Length
FreeMods.Value = FreeMods.Value.Count == availableMods.Length
? Array.Empty<Mod>()
: availableMods;
}
private void updateModDisplay()
{
int currentCount = Current.Value.Count;
int currentCount = FreeMods.Value.Count;
if (currentCount == allAvailableAndValidMods.Count())
if (currentCount == allAvailableAndValidMods.Count() || Freestyle.Value)
{
count.Text = "all";
count.FadeColour(colours.Gray2, 200, Easing.OutQuint);

View File

@ -8,23 +8,18 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Screens.Select;
using osu.Game.Localisation;
using osu.Game.Screens.Select;
namespace osu.Game.Screens.OnlinePlay
{
public partial class FooterButtonFreestyle : FooterButton, IHasCurrentValue<bool>
public partial class FooterButtonFreestyle : FooterButton
{
private readonly BindableWithCurrent<bool> current = new BindableWithCurrent<bool>();
public readonly Bindable<bool> Freestyle = new Bindable<bool>();
public Bindable<bool> Current
{
get => current.Current;
set => current.Current = value;
}
protected override bool IsActive => Freestyle.Value;
public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); }
@ -37,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay
public FooterButtonFreestyle()
{
// Overwrite any external behaviour as we delegate the main toggle action to a sub-button.
base.Action = () => current.Value = !current.Value;
base.Action = () => Freestyle.Value = !Freestyle.Value;
}
[BackgroundDependencyLoader]
@ -81,12 +76,12 @@ namespace osu.Game.Screens.OnlinePlay
{
base.LoadComplete();
Current.BindValueChanged(_ => updateDisplay(), true);
Freestyle.BindValueChanged(_ => updateDisplay(), true);
}
private void updateDisplay()
{
if (current.Value)
if (Freestyle.Value)
{
text.Text = "on";
text.FadeColour(colours.Gray2, 200, Easing.OutQuint);

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@ -440,11 +439,14 @@ namespace osu.Game.Screens.OnlinePlay.Match
var rulesetInstance = GetGameplayRuleset().CreateInstance();
Mod[] allowedMods = item.Freestyle
? rulesetInstance.AllMods.OfType<Mod>().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray()
: item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
// Remove any user mods that are no longer allowed.
Mod[] allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray();
if (!newUserMods.SequenceEqual(UserMods.Value))
UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList();
UserMods.Value = newUserMods;
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
int beatmapId = GetGameplayBeatmap().OnlineID;
@ -455,14 +457,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray();
Ruleset.Value = GetGameplayRuleset();
bool freeMod = item.AllowedMods.Any();
bool freestyle = item.Freestyle;
// For now, the game can never be in a state where freemod and freestyle are on at the same time.
// This will change, but due to the current implementation if this was to occur drawables will overlap so let's assert.
Debug.Assert(!freeMod || !freestyle);
if (freeMod)
if (allowedMods.Length > 0)
{
UserModsSection.Show();
UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType());
@ -474,7 +469,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
UserModsSelectOverlay.IsValidMod = _ => false;
}
if (freestyle)
if (item.Freestyle)
{
UserStyleSection.Show();
@ -487,7 +482,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true)
{
AllowReordering = false,
AllowEditing = freestyle,
AllowEditing = true,
RequestEdit = _ => OpenStyleSelection()
};
}

View File

@ -32,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
protected override void LoadComplete()
{
base.LoadComplete();
QueueItems.BindCollectionChanged((_, _) => Text.Text = QueueItems.Count > 0 ? $"Queue ({QueueItems.Count})" : "Queue", true);
QueueItems.BindCollectionChanged((_, _) => Text.Text = QueueItems.Count > 0 ? $"Up next ({QueueItems.Count})" : "Up next", true);
}
}
}

View File

@ -11,7 +11,6 @@ using osu.Framework.Screens;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
@ -122,9 +121,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.ValidForMultiplayer;
protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.ValidForMultiplayerAsFreeMod;
}
}

View File

@ -98,7 +98,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
new Drawable?[]
{
// Participants column
new GridContainer
{
RelativeSizeAxes = Axes.Both,
@ -118,15 +117,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
}
},
// Spacer
null,
// Beatmap column
new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 5),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[] { new OverlinedHeader("Beatmap") },
new Drawable[] { new OverlinedHeader("Beatmap queue") },
new Drawable[]
{
addItemButton = new AddItemButton
@ -147,80 +153,67 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
SelectedItem = SelectedItem
}
},
new Drawable[]
new[]
{
new Container
UserModsSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = 10 },
Children = new[]
Alpha = 0,
Children = new Drawable[]
{
UserModsSection = new FillFlowContainer
new OverlinedHeader("Extra mods"),
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new OverlinedHeader("Extra mods"),
new FillFlowContainer
new UserModSelectButton
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new UserModSelectButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 90,
Text = "Select",
Action = ShowUserModSelect,
},
new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Current = UserMods,
Scale = new Vector2(0.8f),
},
}
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 90,
Height = 30,
Text = "Select",
Action = ShowUserModSelect,
},
}
},
UserStyleSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
Children = new Drawable[]
{
new OverlinedHeader("Difficulty"),
UserStyleDisplayContainer = new Container<DrawableRoomPlaylistItem>
new ModDisplay
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Current = UserMods,
Scale = new Vector2(0.8f),
},
}
},
}
}
},
new[]
{
UserStyleSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = 10 },
Alpha = 0,
Children = new Drawable[]
{
new OverlinedHeader("Difficulty"),
UserStyleDisplayContainer = new Container<DrawableRoomPlaylistItem>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
},
},
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 5),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
}
},
// Spacer
null,
// Main right column
new GridContainer
{
RelativeSizeAxes = Axes.Both,

View File

@ -161,11 +161,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Right = 70 },
Spacing = new Vector2(2),
Children = new Drawable[]
{
userStyleDisplay = new StyleDisplayIcon(),
userStyleDisplay = new StyleDisplayIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
userModsDisplay = new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Scale = new Vector2(0.5f),
ExpansionMode = ExpansionMode.AlwaysContracted,
}
@ -209,15 +216,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
const double fade_time = 50;
MultiplayerPlaylistItem? currentItem = client.Room.GetCurrentItem();
Ruleset? ruleset = currentItem != null ? rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance() : null;
if (client.Room.GetCurrentItem() is MultiplayerPlaylistItem currentItem)
{
int userBeatmapId = User.BeatmapId ?? currentItem.BeatmapID;
int userRulesetId = User.RulesetId ?? currentItem.RulesetID;
Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance();
int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null;
userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty;
int? currentModeRank = userRuleset == null ? null : User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset.ShortName)?.GlobalRank;
userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty;
if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID)
userStyleDisplay.Style = null;
else
userStyleDisplay.Style = (userBeatmapId, userRulesetId);
// If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187
// This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix.
Schedule(() => userModsDisplay.Current.Value = userRuleset == null ? Array.Empty<Mod>() : User.Mods.Select(m => m.ToMod(userRuleset)).ToList());
}
userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability);
if ((User.BeatmapAvailability.State == DownloadState.LocallyAvailable) && (User.State != MultiplayerUserState.Spectating))
if (User.BeatmapAvailability.State == DownloadState.LocallyAvailable && User.State != MultiplayerUserState.Spectating)
{
userModsDisplay.FadeIn(fade_time);
userStyleDisplay.FadeIn(fade_time);
@ -228,20 +248,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
userStyleDisplay.FadeOut(fade_time);
}
if ((User.BeatmapId == null && User.RulesetId == null) || (User.BeatmapId == currentItem?.BeatmapID && User.RulesetId == currentItem?.RulesetID))
userStyleDisplay.Style = null;
else
userStyleDisplay.Style = (User.BeatmapId ?? currentItem?.BeatmapID ?? 0, User.RulesetId ?? currentItem?.RulesetID ?? 0);
kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0;
crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0;
// If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187
// This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix.
Schedule(() =>
{
userModsDisplay.Current.Value = ruleset != null ? User.Mods.Select(m => m.ToMod(ruleset)).ToList() : Array.Empty<Mod>();
});
}
public MenuItem[]? ContextMenuItems

View File

@ -42,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay
protected override UserActivity InitialActivity => new UserActivity.InLobby(room);
protected readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
protected readonly Bindable<bool> Freestyle = new Bindable<bool>();
protected readonly Bindable<bool> Freestyle = new Bindable<bool>(true);
private readonly Room room;
private readonly PlaylistItem? initialItem;
@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay
freeModSelect = new FreeModSelectOverlay
{
SelectedMods = { BindTarget = FreeMods },
IsValidMod = IsValidFreeMod,
IsValidMod = isValidFreeMod,
};
}
@ -126,6 +126,7 @@ namespace osu.Game.Screens.OnlinePlay
{
if (enabled.NewValue)
{
freeModsFooterButton.Enabled.Value = false;
freeModsFooterButton.Enabled.Value = false;
ModsFooterButton.Enabled.Value = false;
@ -144,10 +145,10 @@ namespace osu.Game.Screens.OnlinePlay
private void onModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
{
FreeMods.Value = FreeMods.Value.Where(checkCompatibleFreeMod).ToList();
FreeMods.Value = FreeMods.Value.Where(isValidFreeMod).ToList();
// Reset the validity delegate to update the overlay's display.
freeModSelect.IsValidMod = IsValidFreeMod;
freeModSelect.IsValidMod = isValidFreeMod;
}
private void onRulesetChanged(ValueChangedEvent<RulesetInfo> ruleset)
@ -194,7 +195,7 @@ namespace osu.Game.Screens.OnlinePlay
protected override ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay(OverlayColourScheme.Plum)
{
IsValidMod = IsValidMod
IsValidMod = isValidMod
};
protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons()
@ -205,8 +206,15 @@ namespace osu.Game.Screens.OnlinePlay
baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[]
{
(freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }, null),
(new FooterButtonFreestyle { Current = Freestyle }, null)
(freeModsFooterButton = new FooterButtonFreeMods(freeModSelect)
{
FreeMods = { BindTarget = FreeMods },
Freestyle = { BindTarget = Freestyle }
}, null),
(new FooterButtonFreestyle
{
Freestyle = { BindTarget = Freestyle }
}, null)
});
return baseButtons;
@ -217,18 +225,18 @@ namespace osu.Game.Screens.OnlinePlay
/// </summary>
/// <param name="mod">The <see cref="Mod"/> to check.</param>
/// <returns>Whether <paramref name="mod"/> is a valid mod for online play.</returns>
protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && ModUtils.FlattenMod(mod).All(m => m.UserPlayable);
private bool isValidMod(Mod mod) => ModUtils.IsValidModForMatchType(mod, room.Type);
/// <summary>
/// Checks whether a given <see cref="Mod"/> is valid for per-player free-mod selection.
/// </summary>
/// <param name="mod">The <see cref="Mod"/> to check.</param>
/// <returns>Whether <paramref name="mod"/> is a selectable free-mod.</returns>
protected virtual bool IsValidFreeMod(Mod mod) => IsValidMod(mod) && checkCompatibleFreeMod(mod);
private bool checkCompatibleFreeMod(Mod mod)
=> Mods.Value.All(m => m.Acronym != mod.Acronym) // Mod must not be contained in the required mods.
&& ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); // Mod must be compatible with all the required mods.
private bool isValidFreeMod(Mod mod) => ModUtils.IsValidFreeModForMatchType(mod, room.Type)
// Mod must not be contained in the required mods.
&& Mods.Value.All(m => m.Acronym != mod.Acronym)
// Mod must be compatible with all the required mods.
&& ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray());
protected override void Dispose(bool isDisposing)
{

View File

@ -146,7 +146,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
new Drawable?[]
{
// Playlist items column
new GridContainer
{
RelativeSizeAxes = Axes.Both,
@ -176,73 +175,67 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
new Dimension(),
}
},
// Spacer
null,
// Middle column (mods and leaderboard)
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
new[]
{
new Container
UserModsSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Bottom = 10 },
Children = new[]
Alpha = 0,
Children = new Drawable[]
{
UserModsSection = new FillFlowContainer
new OverlinedHeader("Extra mods"),
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
Margin = new MarginPadding { Bottom = 10 },
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new OverlinedHeader("Extra mods"),
new FillFlowContainer
new UserModSelectButton
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new UserModSelectButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 90,
Text = "Select",
Action = ShowUserModSelect,
},
new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Current = UserMods,
Scale = new Vector2(0.8f),
},
}
}
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 90,
Height = 30,
Text = "Select",
Action = ShowUserModSelect,
},
new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Current = UserMods,
Scale = new Vector2(0.8f),
},
}
},
UserStyleSection = new FillFlowContainer
}
}
},
},
new[]
{
UserStyleSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Bottom = 10 },
Alpha = 0,
Children = new Drawable[]
{
new OverlinedHeader("Difficulty"),
UserStyleDisplayContainer = new Container<DrawableRoomPlaylistItem>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
Children = new Drawable[]
{
new OverlinedHeader("Difficulty"),
UserStyleDisplayContainer = new Container<DrawableRoomPlaylistItem>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
},
AutoSizeAxes = Axes.Y
}
}
},
},
@ -273,12 +266,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
}
},
// Spacer
null,
// Main right column
new GridContainer
{
RelativeSizeAxes = Axes.Both,

View File

@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@ -17,6 +19,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
using osu.Game.Localisation.HUD;
using osu.Game.Localisation.SkinComponents;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Skinning;
using osuTK;
@ -28,17 +31,17 @@ namespace osu.Game.Screens.Play.HUD
{
private const int max_spectators_displayed = 10;
public BindableList<SpectatorUser> Spectators { get; } = new BindableList<SpectatorUser>();
public Bindable<LocalUserPlayingState> UserPlayingState { get; } = new Bindable<LocalUserPlayingState>();
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))]
public Bindable<Typeface> Font { get; } = new Bindable<Typeface>(Typeface.Torus);
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))]
public BindableColour4 HeaderColour { get; } = new BindableColour4(Colour4.White);
protected OsuSpriteText Header { get; private set; } = null!;
private BindableList<SpectatorUser> watchingUsers { get; } = new BindableList<SpectatorUser>();
private Bindable<LocalUserPlayingState> userPlayingState { get; } = new Bindable<LocalUserPlayingState>();
private int displayedSpectatorCount;
private OsuSpriteText header = null!;
private FillFlowContainer mainFlow = null!;
private FillFlowContainer<SpectatorListEntry> spectatorsFlow = null!;
private DrawablePool<SpectatorListEntry> pool = null!;
@ -49,6 +52,9 @@ namespace osu.Game.Screens.Play.HUD
[Resolved]
private GameplayState gameplayState { get; set; } = null!;
[Resolved]
private MultiplayerClient multiplayerClient { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
@ -63,7 +69,7 @@ namespace osu.Game.Screens.Play.HUD
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Header = new OsuSpriteText
header = new OsuSpriteText
{
Colour = colours.Blue0,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
@ -78,18 +84,18 @@ namespace osu.Game.Screens.Play.HUD
pool = new DrawablePool<SpectatorListEntry>(max_spectators_displayed),
};
HeaderColour.Value = Header.Colour;
HeaderColour.Value = header.Colour;
}
protected override void LoadComplete()
{
base.LoadComplete();
((IBindableList<SpectatorUser>)Spectators).BindTo(client.WatchingUsers);
((IBindable<LocalUserPlayingState>)UserPlayingState).BindTo(gameplayState.PlayingState);
((IBindableList<SpectatorUser>)watchingUsers).BindTo(client.WatchingUsers);
((IBindable<LocalUserPlayingState>)userPlayingState).BindTo(gameplayState.PlayingState);
Spectators.BindCollectionChanged(onSpectatorsChanged, true);
UserPlayingState.BindValueChanged(_ => updateVisibility());
watchingUsers.BindCollectionChanged(onSpectatorsChanged, true);
userPlayingState.BindValueChanged(_ => updateVisibility());
Font.BindValueChanged(_ => updateAppearance());
HeaderColour.BindValueChanged(_ => updateAppearance(), true);
@ -100,6 +106,20 @@ namespace osu.Game.Screens.Play.HUD
private void onSpectatorsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
// the multiplayer gameplay leaderboard relies on calling `SpectatorClient.WatchUser()` to get updates on users' total scores.
// this has an unfortunate side effect of other players showing up in `SpectatorClient.WatchingUsers`.
//
// we do not generally wish to display other players in the room as spectators due to that implementation detail,
// therefore this code is intended to filter out those players on the client side.
//
// note that the way that this is done is rather specific to the multiplayer use case and therefore carries a lot of assumptions
// (e.g. that the `MultiplayerRoomUser`s have the correct `State` at the point wherein they issue the `WatchUser()` calls).
// the more proper way to do this (which is by subscribing to `WatchingUsers` and `RoomUpdated`, and doing a proper diff to a third list on any change of either)
// is a lot more difficult to write correctly, given that we also rely on `BindableList`'s collection changed event arguments to properly animate this component.
var excludedUserIds = new HashSet<int>();
if (multiplayerClient.Room != null)
excludedUserIds.UnionWith(multiplayerClient.Room.Users.Where(u => u.State != MultiplayerUserState.Spectating).Select(u => u.UserID));
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
@ -109,6 +129,9 @@ namespace osu.Game.Screens.Play.HUD
var spectator = (SpectatorUser)e.NewItems![i]!;
int index = Math.Max(e.NewStartingIndex, 0) + i;
if (excludedUserIds.Contains(spectator.OnlineID))
continue;
if (index >= max_spectators_displayed)
break;
@ -125,10 +148,10 @@ namespace osu.Game.Screens.Play.HUD
for (int i = 0; i < spectatorsFlow.Count; i++)
spectatorsFlow.SetLayoutPosition(spectatorsFlow[i], i);
if (Spectators.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed)
if (watchingUsers.Count >= max_spectators_displayed && spectatorsFlow.Count < max_spectators_displayed)
{
for (int i = spectatorsFlow.Count; i < max_spectators_displayed; i++)
addNewSpectatorToList(i, Spectators[i]);
addNewSpectatorToList(i, watchingUsers[i]);
}
break;
@ -144,7 +167,8 @@ namespace osu.Game.Screens.Play.HUD
throw new NotSupportedException();
}
Header.Text = SpectatorListStrings.SpectatorCount(Spectators.Count).ToUpper();
displayedSpectatorCount = watchingUsers.Count(s => !excludedUserIds.Contains(s.OnlineID));
header.Text = SpectatorListStrings.SpectatorCount(displayedSpectatorCount).ToUpper();
updateVisibility();
for (int i = 0; i < spectatorsFlow.Count; i++)
@ -160,7 +184,7 @@ namespace osu.Game.Screens.Play.HUD
var entry = pool.Get(entry =>
{
entry.Current.Value = spectator;
entry.UserPlayingState = UserPlayingState;
entry.UserPlayingState = userPlayingState;
});
spectatorsFlow.Insert(i, entry);
@ -169,15 +193,15 @@ namespace osu.Game.Screens.Play.HUD
private void updateVisibility()
{
// We don't want to show spectators when we are watching a replay.
mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint);
mainFlow.FadeTo(displayedSpectatorCount > 0 && userPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint);
}
private void updateAppearance()
{
Header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold);
Header.Colour = HeaderColour.Value;
header.Font = OsuFont.GetFont(Font.Value, 12, FontWeight.Bold);
header.Colour = HeaderColour.Value;
Width = Header.DrawWidth;
Width = header.DrawWidth;
}
private partial class SpectatorListEntry : PoolableDrawable

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
@ -35,7 +34,32 @@ namespace osu.Game.Screens.Ranking
return null;
getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset);
getScoreRequest.Success += r => scoresCallback.Invoke(r.Scores.Where(s => !s.MatchesOnlineID(Score)).Select(s => s.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo)));
getScoreRequest.Success += r =>
{
var toDisplay = new List<ScoreInfo>();
for (int i = 0; i < r.Scores.Count; ++i)
{
var score = r.Scores[i];
int position = i + 1;
if (score.MatchesOnlineID(Score))
{
// we don't want to add the same score twice, but also setting any properties of `Score` this late will have no visible effect,
// so we have to fish out the actual drawable panel and set the position to it directly.
var panel = ScorePanelList.GetPanelForScore(Score);
Score.Position = panel.ScorePosition.Value = position;
}
else
{
var converted = score.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo);
converted.Position = position;
toDisplay.Add(converted);
}
}
scoresCallback.Invoke(toDisplay);
};
return getScoreRequest;
}

View File

@ -25,6 +25,11 @@ namespace osu.Game.Screens.Select
protected static readonly Vector2 SHEAR = new Vector2(SHEAR_WIDTH / Footer.HEIGHT, 0);
/// <summary>
/// Used to show an initial animation hinting at the enabled state.
/// </summary>
protected virtual bool IsActive => false;
public LocalisableString Text
{
get => SpriteText?.Text ?? default;
@ -124,6 +129,18 @@ namespace osu.Game.Screens.Select
{
base.LoadComplete();
Enabled.BindValueChanged(_ => updateDisplay(), true);
if (IsActive)
{
box.ClearTransforms();
using (box.BeginDelayedSequence(200))
{
box.FadeIn(200)
.Then()
.FadeOut(1500, Easing.OutQuint);
}
}
}
public Action Hovered;

View File

@ -8,6 +8,7 @@ using System.Linq;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@ -292,5 +293,45 @@ namespace osu.Game.Utils
return rate;
}
/// <summary>
/// Determines whether a mod can be applied to playlist items in the given match type.
/// </summary>
/// <param name="mod">The mod to test.</param>
/// <param name="type">The match type.</param>
public static bool IsValidModForMatchType(Mod mod, MatchType type)
{
if (mod.Type == ModType.System || !mod.UserPlayable || !mod.HasImplementation)
return false;
switch (type)
{
case MatchType.Playlists:
return true;
default:
return mod.ValidForMultiplayer;
}
}
/// <summary>
/// Determines whether a mod can be applied as a free mod to playlist items in the given match type.
/// </summary>
/// <param name="mod">The mod to test.</param>
/// <param name="type">The match type.</param>
public static bool IsValidFreeModForMatchType(Mod mod, MatchType type)
{
if (mod.Type == ModType.System || !mod.UserPlayable || !mod.HasImplementation)
return false;
switch (type)
{
case MatchType.Playlists:
return true;
default:
return mod.ValidForMultiplayerAsFreeMod;
}
}
}
}

View File

@ -41,7 +41,7 @@ namespace osu.iOS
updateOrientation();
}
private void updateOrientation()
private void updateOrientation() => UIApplication.SharedApplication.InvokeOnMainThread(() =>
{
bool iPad = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad;
var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, iPad);
@ -60,7 +60,7 @@ namespace osu.iOS
appDelegate.Orientations = null;
break;
}
}
});
protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier();