1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-20 16:52:59 +08:00

Merge branch 'master' into pp-dev

This commit is contained in:
Dean Herbert 2025-02-17 11:59:14 +09:00
commit ab88ccb081
No known key found for this signature in database
86 changed files with 2480 additions and 559 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

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="osu.Android">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

View File

@ -12,6 +12,7 @@ using osu.Game;
using osu.Game.Screens;
using osu.Game.Updater;
using osu.Game.Utils;
using osuTK;
namespace osu.Android
{
@ -20,6 +21,8 @@ namespace osu.Android
[Cached]
private readonly OsuGameActivity gameActivity;
protected override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth);
public OsuGameAndroid(OsuGameActivity activity)
: base(null)
{

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

@ -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

@ -73,6 +73,8 @@ namespace osu.Game.Tests.Skins
"Archives/modified-default-20241207.osk",
// Covers skinnable spectator list
"Archives/modified-argon-20250116.osk",
// Covers player team flag
"Archives/modified-argon-20250214.osk",
};
/// <summary>

View File

@ -376,6 +376,6 @@ namespace osu.Game.Tests.Visual.Collections
private void assertCollectionName(int index, string name)
=> AddUntilStep($"item {index + 1} has correct name",
() => dialog.ChildrenOfType<DrawableCollectionList>().Single().OrderedItems.ElementAt(index).ChildrenOfType<TextBox>().First().Text == name);
() => dialog.ChildrenOfType<DrawableCollectionList>().Single().OrderedItems.ElementAtOrDefault(index)?.ChildrenOfType<TextBox>().First().Text == name);
}
}

View File

@ -24,7 +24,11 @@ namespace osu.Game.Tests.Visual.Editing
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) },
CachedDependencies = new[]
{
(typeof(ScreenFooter), (object)footer),
(typeof(BeatmapSubmissionSettings), new BeatmapSubmissionSettings()),
},
Children = new Drawable[]
{
receptor,

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

@ -346,6 +346,13 @@ namespace osu.Game.Tests.Visual.Online
Twitter = "test_user",
Discord = "test_user",
Website = "https://google.com",
Team = new APITeam
{
Id = 1,
Name = "Collective Wangs",
ShortName = "WANG",
FlagUrl = "https://assets.ppy.sh/teams/logo/1/wanglogo.jpg",
}
};
}
}

View File

@ -0,0 +1,113 @@
// 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.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Playlists;
using osuTK;
using osuTK.Input;
using SharpCompress;
namespace osu.Game.Tests.Visual.Playlists
{
public partial class TestSceneAddPlaylistToCollectionButton : OsuManualInputManagerTestScene
{
private BeatmapManager manager = null!;
private BeatmapSetInfo importedBeatmap = null!;
private Room room = null!;
private AddPlaylistToCollectionButton button = null!;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
Add(notificationOverlay);
}
[Cached(typeof(INotificationOverlay))]
private NotificationOverlay notificationOverlay = new NotificationOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
};
[SetUpSteps]
public void SetUpSteps()
{
AddStep("clear realm", () => Realm.Realm.Write(() => Realm.Realm.RemoveAll<BeatmapCollection>()));
AddStep("clear notifications", () => notificationOverlay.AllNotifications.Empty());
importBeatmap();
setupRoom();
AddStep("create button", () =>
{
Add(button = new AddPlaylistToCollectionButton(room)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(300, 40),
});
});
}
[Test]
public void TestButtonFlow()
{
AddStep("move mouse to button", () => InputManager.MoveMouseTo(button));
AddStep("click button", () => InputManager.Click(MouseButton.Left));
AddUntilStep("notification shown", () => notificationOverlay.AllNotifications.Any(n => n.Text.ToString().StartsWith("Created new collection", StringComparison.Ordinal)));
AddUntilStep("realm is updated", () => Realm.Realm.All<BeatmapCollection>().FirstOrDefault(c => c.Name == room.Name) != null);
}
private void importBeatmap() => AddStep("import beatmap", () =>
{
var beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);
Debug.Assert(beatmap.BeatmapInfo.BeatmapSet != null);
importedBeatmap = manager.Import(beatmap.BeatmapInfo.BeatmapSet)!.Value.Detach();
});
private void setupRoom() => AddStep("setup room", () =>
{
room = new Room
{
Name = "my awesome room",
MaxAttempts = 5,
Host = API.LocalUser.Value
};
room.RecentParticipants = [room.Host];
room.EndDate = DateTimeOffset.Now.AddMinutes(5);
room.Playlist =
[
new PlaylistItem(importedBeatmap.Beatmaps.First())
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}
];
});
}
}

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

@ -238,7 +238,7 @@ namespace osu.Game.Configuration
public void Migrate()
{
// arrives as 2020.123.0
// arrives as 2020.123.0-lazer
string rawVersion = Get<string>(OsuSetting.Version);
if (rawVersion.Length < 6)
@ -251,11 +251,14 @@ namespace osu.Game.Configuration
if (!int.TryParse(pieces[0], out int year)) return;
if (!int.TryParse(pieces[1], out int monthDay)) return;
// ReSharper disable once UnusedVariable
int combined = (year * 10000) + monthDay;
int combined = year * 10000 + monthDay;
// migrations can be added here using a condition like:
// if (combined < 20220103) { performMigration() }
if (combined < 20250214)
{
// UI scaling on mobile platforms has been internally adjusted such that 1x UI scale looks correctly zoomed in than before.
if (RuntimeInfo.IsMobile)
GetBindable<float>(OsuSetting.UIScale).SetDefault();
}
}
public override TrackedSettings CreateTrackedSettings()

View File

@ -61,6 +61,20 @@ namespace osu.Game.Database
Configuration = new LegacySkinDecoder().Decode(skinStreamReader)
};
MutateBeatmap(model, playableBeatmap);
// Encode to legacy format
var stream = new MemoryStream();
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin).Encode(sw);
stream.Seek(0, SeekOrigin.Begin);
return stream;
}
protected virtual void MutateBeatmap(BeatmapSetInfo beatmapSet, IBeatmap playableBeatmap)
{
// Convert beatmap elements to be compatible with legacy format
// So we truncate time and position values to integers, and convert paths with multiple segments to Bézier curves
@ -145,15 +159,6 @@ namespace osu.Game.Database
hasPath.Path.ControlPoints.Add(new PathControlPoint(position));
}
}
// Encode to legacy format
var stream = new MemoryStream();
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin).Encode(sw);
stream.Seek(0, SeekOrigin.Begin);
return stream;
}
protected override string FileExtension => @".osz";

View File

@ -1,8 +1,6 @@
// 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.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -26,17 +24,17 @@ namespace osu.Game.Graphics.Containers
{
internal const float TRANSITION_DURATION = 500;
private Bindable<float> sizeX;
private Bindable<float> sizeY;
private Bindable<float> posX;
private Bindable<float> posY;
private Bindable<bool> applySafeAreaPadding;
private Bindable<float> sizeX = null!;
private Bindable<float> sizeY = null!;
private Bindable<float> posX = null!;
private Bindable<float> posY = null!;
private Bindable<bool> applySafeAreaPadding = null!;
private Bindable<MarginPadding> safeAreaPadding;
private Bindable<MarginPadding> safeAreaPadding = null!;
private readonly ScalingMode? targetMode;
private Bindable<ScalingMode> scalingMode;
private Bindable<ScalingMode> scalingMode = null!;
private readonly Container content;
protected override Container<Drawable> Content => content;
@ -45,9 +43,9 @@ namespace osu.Game.Graphics.Containers
private readonly Container sizableContainer;
private BackgroundScreenStack backgroundStack;
private BackgroundScreenStack? backgroundStack;
private Bindable<float> scalingMenuBackgroundDim;
private Bindable<float> scalingMenuBackgroundDim = null!;
private RectangleF? customRect;
private bool customRectIsRelativePosition;
@ -88,7 +86,8 @@ namespace osu.Game.Graphics.Containers
public partial class ScalingDrawSizePreservingFillContainer : DrawSizePreservingFillContainer
{
private readonly bool applyUIScale;
private Bindable<float> uiScale;
private Bindable<float>? uiScale;
protected float CurrentScale { get; private set; } = 1;
@ -99,6 +98,9 @@ namespace osu.Game.Graphics.Containers
this.applyUIScale = applyUIScale;
}
[Resolved(canBeNull: true)]
private OsuGame? game { get; set; }
[BackgroundDependencyLoader]
private void load(OsuConfigManager osuConfig)
{
@ -111,6 +113,8 @@ namespace osu.Game.Graphics.Containers
protected override void Update()
{
if (game != null)
TargetDrawSize = game.ScalingContainerTargetDrawSize;
Scale = new Vector2(CurrentScale);
Size = new Vector2(1 / CurrentScale);
@ -233,13 +237,13 @@ namespace osu.Game.Graphics.Containers
private partial class SizeableAlwaysInputContainer : Container
{
[Resolved]
private GameHost host { get; set; }
private GameHost host { get; set; } = null!;
[Resolved]
private ISafeArea safeArea { get; set; }
private ISafeArea safeArea { get; set; } = null!;
[Resolved]
private OsuConfigManager config { get; set; }
private OsuConfigManager config { get; set; } = null!;
private readonly bool confineHostCursor;
private readonly LayoutValue cursorRectCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);

View File

@ -18,6 +18,8 @@ namespace osu.Game.Graphics.UserInterface
public abstract partial class OsuSliderBar<T> : SliderBar<T>, IHasTooltip
where T : struct, INumber<T>, IMinMaxValue<T>
{
public override bool AcceptsFocus => !Current.Disabled;
public bool PlaySamplesOnAdjust { get; set; } = true;
/// <summary>

View File

@ -162,6 +162,8 @@ namespace osu.Game.Graphics.UserInterface
protected partial class BoundSlider : RoundedSliderBar<double>
{
public override bool AcceptsFocus => false;
public new Nub Nub => base.Nub;
public string? DefaultString;

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

@ -39,6 +39,31 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString SubmissionSettings => new TranslatableString(getKey(@"submission_settings"), @"Submission settings");
/// <summary>
/// "Submit beatmap!"
/// </summary>
public static LocalisableString ConfirmSubmission => new TranslatableString(getKey(@"confirm_submission"), @"Submit beatmap!");
/// <summary>
/// "Exporting beatmap for compatibility..."
/// </summary>
public static LocalisableString Exporting => new TranslatableString(getKey(@"exporting"), @"Exporting beatmap for compatibility...");
/// <summary>
/// "Preparing for upload..."
/// </summary>
public static LocalisableString Preparing => new TranslatableString(getKey(@"preparing"), @"Preparing for upload...");
/// <summary>
/// "Uploading beatmap contents..."
/// </summary>
public static LocalisableString Uploading => new TranslatableString(getKey(@"uploading"), @"Uploading beatmap contents...");
/// <summary>
/// "Finishing up..."
/// </summary>
public static LocalisableString Finishing => new TranslatableString(getKey(@"finishing"), @"Finishing up...");
/// <summary>
/// "Before you continue, we ask you to check whether the content you are uploading has been cleared for upload. Please understand that you are responsible for the content you upload to the platform and if in doubt, should ask permission from the creators before uploading!"
/// </summary>
@ -115,9 +140,24 @@ namespace osu.Game.Localisation
public static LocalisableString LoadInBrowserAfterSubmission => new TranslatableString(getKey(@"load_in_browser_after_submission"), @"Load in browser after submission");
/// <summary>
/// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."
/// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."
/// </summary>
public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost.");
public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost.");
/// <summary>
/// "Empty beatmaps cannot be submitted."
/// </summary>
public static LocalisableString EmptyBeatmapsCannotBeSubmitted => new TranslatableString(getKey(@"empty_beatmaps_cannot_be_submitted"), @"Empty beatmaps cannot be submitted.");
/// <summary>
/// "Update beatmap!"
/// </summary>
public static LocalisableString UpdateBeatmap => new TranslatableString(getKey(@"update_beatmap"), @"Update beatmap!");
/// <summary>
/// "Upload NEW beatmap!"
/// </summary>
public static LocalisableString UploadNewBeatmap => new TranslatableString(getKey(@"upload_new_beatmap"), @"Upload NEW beatmap!");
private static string getKey(string key) => $@"{prefix}:{key}";
}

View File

@ -69,6 +69,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString DeleteDifficulty => new TranslatableString(getKey(@"delete_difficulty"), @"Delete difficulty");
/// <summary>
/// "Edit externally"
/// </summary>
public static LocalisableString EditExternally => new TranslatableString(getKey(@"edit_externally"), @"Edit externally");
/// <summary>
/// "Submit beatmap"
/// </summary>
public static LocalisableString SubmitBeatmap => new TranslatableString(getKey(@"submit_beatmap"), @"Submit beatmap");
/// <summary>
/// "setup"
/// </summary>
@ -184,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

@ -26,10 +26,8 @@ namespace osu.Game.Online.API.Requests
public uint BeatmapSetID { get; }
// ReSharper disable once CollectionNeverUpdated.Global
public Dictionary<string, byte[]> FilesChanged { get; } = new Dictionary<string, byte[]>();
// ReSharper disable once CollectionNeverUpdated.Global
public HashSet<string> FilesDeleted { get; } = new HashSet<string>();
public PatchBeatmapPackageRequest(uint beatmapSetId)
@ -48,7 +46,7 @@ namespace osu.Game.Online.API.Requests
foreach (string filename in FilesDeleted)
request.AddParameter(@"filesDeleted", filename, RequestParameterType.Form);
request.Timeout = 60_000;
request.Timeout = 600_000;
return request;
}
}

View File

@ -38,7 +38,7 @@ namespace osu.Game.Online.API.Requests
var request = base.CreateWebRequest();
request.AddFile(@"beatmapArchive", oszPackage);
request.Method = HttpMethod.Put;
request.Timeout = 60_000;
request.Timeout = 600_000;
return request;
}
}

View File

@ -0,0 +1,23 @@
// 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 Newtonsoft.Json;
namespace osu.Game.Online.API.Requests.Responses
{
[JsonObject(MemberSerialization.OptIn)]
public class APITeam
{
[JsonProperty(@"id")]
public int Id { get; set; } = 1;
[JsonProperty(@"name")]
public string Name { get; set; } = string.Empty;
[JsonProperty(@"short_name")]
public string ShortName { get; set; } = string.Empty;
[JsonProperty(@"flag_url")]
public string FlagUrl = string.Empty;
}
}

View File

@ -55,6 +55,10 @@ namespace osu.Game.Online.API.Requests.Responses
set => countryCodeString = value.ToString();
}
[JsonProperty(@"team")]
[CanBeNull]
public APITeam Team { get; set; }
[JsonProperty(@"profile_colour")]
public string Colour;

View File

@ -13,6 +13,7 @@ namespace osu.Game.Online
SpectatorUrl = $@"{APIUrl}/signalr/spectator";
MultiplayerUrl = $@"{APIUrl}/signalr/multiplayer";
MetadataUrl = $@"{APIUrl}/signalr/metadata";
BeatmapSubmissionServiceUrl = $@"{APIUrl}/beatmap-submission";
}
}
}

View File

@ -180,6 +180,7 @@ namespace osu.Game.Online.Leaderboards
Height = 28,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10f, 0f),
Margin = new MarginPadding { Bottom = -2 },
Children = new Drawable[]
{
flagBadgeAndDateContainer = new FillFlowContainer
@ -189,7 +190,7 @@ namespace osu.Game.Online.Leaderboards
RelativeSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5f, 0f),
Width = 87f,
Width = 114f,
Masking = true,
Children = new Drawable[]
{
@ -199,6 +200,12 @@ namespace osu.Game.Online.Leaderboards
Origin = Anchor.CentreLeft,
Size = new Vector2(28, 20),
},
new UpdateableTeamFlag(user.Team)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(40, 20),
},
new DateLabel(Score.Date)
{
Anchor = Anchor.CentreLeft,
@ -206,15 +213,6 @@ namespace osu.Game.Online.Leaderboards
},
},
},
new FillFlowContainer
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Left = edge_margin },
Children = statisticsLabels
},
},
},
},
@ -234,6 +232,7 @@ namespace osu.Game.Online.Leaderboards
GlowColour = Color4Extensions.FromHex(@"83ccfa"),
Current = scoreManager.GetBindableTotalScoreString(Score),
Font = OsuFont.Numeric.With(size: 23),
Margin = new MarginPadding { Top = 1 },
},
RankContainer = new Container
{
@ -250,13 +249,32 @@ namespace osu.Game.Online.Leaderboards
},
},
},
modsContainer = new FillFlowContainer<ModIcon>
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
ChildrenEnumerable = Score.Mods.AsOrdered().Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) })
Children = new Drawable[]
{
new FillFlowContainer
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Left = edge_margin },
Children = statisticsLabels
},
modsContainer = new FillFlowContainer<ModIcon>
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
ChildrenEnumerable = Score.Mods.AsOrdered().Select(mod => new ModIcon(mod) { Scale = new Vector2(0.34f) })
},
}
},
},
},
@ -324,7 +342,7 @@ namespace osu.Game.Online.Leaderboards
private partial class ScoreComponentLabel : Container, IHasTooltip
{
private const float icon_size = 20;
private const float icon_size = 16;
private readonly FillFlowContainer content;
public override bool Contains(Vector2 screenSpacePos) => content.Contains(screenSpacePos);
@ -340,7 +358,7 @@ namespace osu.Game.Online.Leaderboards
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Padding = new MarginPadding { Right = 10 },
Padding = new MarginPadding { Right = 5 },
Children = new Drawable[]
{
new Container
@ -375,7 +393,8 @@ namespace osu.Game.Online.Leaderboards
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = statistic.Value,
Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold, fixedWidth: true)
Spacing = new Vector2(-1, 0),
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, fixedWidth: true)
},
},
};
@ -406,7 +425,7 @@ namespace osu.Game.Online.Leaderboards
public DateLabel(DateTimeOffset date)
: base(date)
{
Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold, italics: true);
Font = OsuFont.GetFont(size: 13, weight: FontWeight.Bold, italics: true);
}
protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30));

View File

@ -72,6 +72,7 @@ using osu.Game.Skinning;
using osu.Game.Updater;
using osu.Game.Users;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
using Sentry;
@ -813,6 +814,12 @@ namespace osu.Game
protected virtual UpdateManager CreateUpdateManager() => new UpdateManager();
/// <summary>
/// Adjust the globally applied <see cref="DrawSizePreservingFillContainer.TargetDrawSize"/> in every <see cref="ScalingContainer"/>.
/// Useful for changing how the game handles different aspect ratios.
/// </summary>
protected internal virtual Vector2 ScalingContainerTargetDrawSize { get; } = new Vector2(1024, 768);
protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything);
#region Beatmap progression

View File

@ -160,7 +160,20 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
{
Size = new Vector2(19, 14),
},
username,
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4),
Children = new Drawable[]
{
new UpdateableTeamFlag(score.User.Team)
{
Size = new Vector2(28, 14),
},
username,
}
},
#pragma warning disable 618
new StatisticText(score.MaxCombo, score.BeatmapInfo!.MaxCombo, @"0\x"),
#pragma warning restore 618

View File

@ -27,7 +27,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
private readonly UpdateableAvatar avatar;
private readonly LinkFlowContainer usernameText;
private readonly DrawableDate achievedOn;
private readonly UpdateableFlag flag;
private readonly UpdateableTeamFlag teamFlag;
public TopScoreUserSection()
{
@ -112,12 +114,30 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
},
}
},
flag = new UpdateableFlag
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(19, 14),
Margin = new MarginPadding { Top = 3 }, // makes spacing look more even
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4),
Children = new Drawable[]
{
flag = new UpdateableFlag
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(19, 14),
Margin = new MarginPadding { Top = 3 }, // makes spacing look more even
},
teamFlag = new UpdateableTeamFlag
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(28, 14),
Margin = new MarginPadding { Top = 3 }, // makes spacing look more even
},
}
},
}
}
@ -139,6 +159,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
{
avatar.User = value.User;
flag.CountryCode = value.User.CountryCode;
teamFlag.Team = value.User.Team;
achievedOn.Date = value.Date;
usernameText.Clear();

View File

@ -80,7 +80,7 @@ namespace osu.Game.Overlays
protected override CountryCode GetCountryCode(APIUser item) => item.CountryCode;
protected override Drawable CreateFlagContent(APIUser item)
protected override Drawable[] CreateFlagContent(APIUser item)
{
var username = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: TEXT_SIZE, italics: true))
{
@ -89,7 +89,7 @@ namespace osu.Game.Overlays
TextAnchor = Anchor.CentreLeft
};
username.AddUserLink(item);
return username;
return [username];
}
}
}

View File

@ -44,6 +44,8 @@ namespace osu.Game.Overlays.Profile.Header
private UpdateableFlag userFlag = null!;
private OsuHoverContainer userCountryContainer = null!;
private OsuSpriteText userCountryText = null!;
private UpdateableTeamFlag teamFlag = null!;
private OsuSpriteText teamText = null!;
private GroupBadgeFlow groupBadgeFlow = null!;
private ToggleCoverButton coverToggle = null!;
private PreviousUsernamesDisplay previousUsernamesDisplay = null!;
@ -154,29 +156,58 @@ namespace osu.Game.Overlays.Profile.Header
titleText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular),
Margin = new MarginPadding { Bottom = 5 }
Margin = new MarginPadding { Bottom = 3 },
},
new FillFlowContainer
{
Margin = new MarginPadding { Top = 3 },
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
userFlag = new UpdateableFlag
{
Size = new Vector2(28, 20),
},
userCountryContainer = new OsuHoverContainer
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = 5 },
Child = userCountryText = new OsuSpriteText
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4, 0),
Children = new Drawable[]
{
Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular),
},
userFlag = new UpdateableFlag
{
Size = new Vector2(28, 20),
},
userCountryContainer = new OsuHoverContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Child = userCountryText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular),
},
},
}
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4, 0),
Children = new Drawable[]
{
teamFlag = new UpdateableTeamFlag
{
Size = new Vector2(40, 20),
},
teamText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular),
},
}
}
}
},
}
@ -217,6 +248,8 @@ namespace osu.Game.Overlays.Profile.Header
userFlag.CountryCode = user?.CountryCode ?? default;
userCountryText.Text = (user?.CountryCode ?? default).GetDescription();
userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default);
teamFlag.Team = user?.Team;
teamText.Text = user?.Team?.Name ?? string.Empty;
supporterTag.SupportLevel = user?.SupportLevel ?? 0;
titleText.Text = user?.Title ?? string.Empty;
titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff");

View File

@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Rankings.Tables
protected override CountryCode GetCountryCode(CountryStatistics item) => item.Code;
protected override Drawable CreateFlagContent(CountryStatistics item) => new CountryName(item.Code);
protected override Drawable[] CreateFlagContent(CountryStatistics item) => [new CountryName(item.Code)];
protected override Drawable[] CreateAdditionalContent(CountryStatistics item) => new Drawable[]
{

View File

@ -80,7 +80,7 @@ namespace osu.Game.Overlays.Rankings.Tables
protected abstract CountryCode GetCountryCode(TModel item);
protected abstract Drawable CreateFlagContent(TModel item);
protected abstract Drawable[] CreateFlagContent(TModel item);
private OsuSpriteText createIndexDrawable(int index) => new RowText
{
@ -92,16 +92,13 @@ namespace osu.Game.Overlays.Rankings.Tables
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Spacing = new Vector2(5, 0),
Margin = new MarginPadding { Bottom = row_spacing },
Children = new[]
{
new UpdateableFlag(GetCountryCode(item))
{
Size = new Vector2(28, 20),
},
CreateFlagContent(item)
}
Children =
[
new UpdateableFlag(GetCountryCode(item)) { Size = new Vector2(28, 20) },
..CreateFlagContent(item)
]
};
protected class RankingsTableColumn : TableColumn

View File

@ -14,6 +14,8 @@ using osu.Game.Users;
using osu.Game.Scoring;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users.Drawables;
using osuTK;
namespace osu.Game.Overlays.Rankings.Tables
{
@ -61,7 +63,7 @@ namespace osu.Game.Overlays.Rankings.Tables
protected sealed override CountryCode GetCountryCode(UserStatistics item) => item.User.CountryCode;
protected sealed override Drawable CreateFlagContent(UserStatistics item)
protected sealed override Drawable[] CreateFlagContent(UserStatistics item)
{
var username = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: TEXT_SIZE, italics: true))
{
@ -70,7 +72,7 @@ namespace osu.Game.Overlays.Rankings.Tables
TextAnchor = Anchor.CentreLeft
};
username.AddUserLink(item.User);
return username;
return [new UpdateableTeamFlag(item.User.Team) { Size = new Vector2(40, 20) }, username];
}
protected sealed override Drawable[] CreateAdditionalContent(UserStatistics item) => new[]

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Game.Graphics;
@ -54,6 +55,10 @@ namespace osu.Game.Overlays
private IconButton expandButton = null!;
private InputManager inputManager = null!;
private Drawable? draggedChild;
/// <summary>
/// Create a new instance.
/// </summary>
@ -125,6 +130,8 @@ namespace osu.Game.Overlays
{
base.LoadComplete();
inputManager = GetContainingInputManager()!;
Expanded.BindValueChanged(_ => updateExpandedState(true));
updateExpandedState(false);
@ -156,6 +163,13 @@ namespace osu.Game.Overlays
headerText.FadeTo(headerText.DrawWidth < DrawWidth ? 1 : 0, 150, Easing.OutQuint);
headerTextVisibilityCache.Validate();
}
// Dragged child finished its drag operation.
if (draggedChild != null && inputManager.DraggedDrawable != draggedChild)
{
draggedChild = null;
updateExpandedState(true);
}
}
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
@ -168,11 +182,17 @@ namespace osu.Game.Overlays
private void updateExpandedState(bool animate)
{
// before we collapse down, let's double check the user is not dragging a UI control contained within us.
if (inputManager.DraggedDrawable.IsRootedAt(this))
{
draggedChild = inputManager.DraggedDrawable;
}
// clearing transforms is necessary to avoid a previous height transform
// potentially continuing to get processed while content has changed to autosize.
content.ClearTransforms();
if (Expanded.Value || IsHovered)
if (Expanded.Value || IsHovered || draggedChild != null)
{
content.AutoSizeAxes = Axes.Y;
content.AutoSizeDuration = animate ? transition_duration : 0;

View File

@ -7,8 +7,10 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Overlays.Toolbar
{
@ -17,6 +19,8 @@ namespace osu.Game.Overlays.Toolbar
private OsuSpriteText realTime;
private OsuSpriteText gameTime;
private FillFlowContainer runningText;
private bool showRuntime = true;
public bool ShowRuntime
@ -52,17 +56,36 @@ namespace osu.Game.Overlays.Toolbar
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AutoSizeAxes = Axes.Y;
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
realTime = new OsuSpriteText(),
gameTime = new OsuSpriteText
realTime = new OsuSpriteText
{
Font = OsuFont.Default.With(fixedWidth: true),
Spacing = new Vector2(-1.5f, 0),
},
runningText = new FillFlowContainer
{
Y = 14,
Colour = colours.PinkLight,
Font = OsuFont.Default.With(size: 10, weight: FontWeight.SemiBold),
}
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(2, 0),
Children = new Drawable[]
{
new OsuSpriteText
{
Text = "running",
Font = OsuFont.Default.With(size: 10, weight: FontWeight.SemiBold),
},
gameTime = new OsuSpriteText
{
Font = OsuFont.Default.With(size: 10, fixedWidth: true, weight: FontWeight.SemiBold),
Spacing = new Vector2(-0.5f, 0),
}
}
},
};
updateMetrics();
@ -71,14 +94,12 @@ namespace osu.Game.Overlays.Toolbar
protected override void UpdateDisplay(DateTimeOffset now)
{
realTime.Text = now.ToLocalisableString(use24HourDisplay ? @"HH:mm:ss" : @"h:mm:ss tt");
gameTime.Text = $"running {new TimeSpan(TimeSpan.TicksPerSecond * (int)(Clock.CurrentTime / 1000)):c}";
gameTime.Text = $"{new TimeSpan(TimeSpan.TicksPerSecond * (int)(Clock.CurrentTime / 1000)):c}";
}
private void updateMetrics()
{
Width = showRuntime || !use24HourDisplay ? 66 : 45; // Allows for space for game time up to 99 days (in the padding area since this is quite rare).
gameTime.FadeTo(showRuntime ? 1 : 0);
runningText.FadeTo(showRuntime ? 1 : 0);
}
}
}

View File

@ -45,6 +45,8 @@ namespace osu.Game.Overlays
private LoadingSpinner loading = null!;
private ScheduledDelegate? loadingShowDelegate;
public bool Completed { get; private set; }
protected WizardOverlay(OverlayColourScheme scheme)
: base(scheme)
{
@ -221,6 +223,7 @@ namespace osu.Game.Overlays
else
{
CurrentStepIndex = null;
Completed = true;
Hide();
}

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

@ -32,6 +32,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
@ -52,6 +53,7 @@ using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Edit.Design;
using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Edit.Submission;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Screens.Edit.Verify;
using osu.Game.Screens.OnlinePlay;
@ -111,6 +113,10 @@ namespace osu.Game.Screens.Edit
[Resolved(canBeNull: true)]
private INotificationOverlay notifications { get; set; }
[Resolved(canBeNull: true)]
[CanBeNull]
private LoginOverlay loginOverlay { get; set; }
[Resolved]
private RealmAccess realm { get; set; }
@ -1251,11 +1257,31 @@ namespace osu.Game.Screens.Edit
if (RuntimeInfo.IsDesktop)
{
var externalEdit = new EditorMenuItem("Edit externally", MenuItemType.Standard, editExternally);
var externalEdit = new EditorMenuItem(EditorStrings.EditExternally, MenuItemType.Standard, editExternally);
saveRelatedMenuItems.Add(externalEdit);
yield return externalEdit;
}
bool isSetMadeOfLegacyRulesetBeatmaps = (isNewBeatmap && Ruleset.Value.IsLegacyRuleset())
|| (!isNewBeatmap && Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Ruleset.IsLegacyRuleset()));
bool submissionAvailable = api.Endpoints.BeatmapSubmissionServiceUrl != null;
if (isSetMadeOfLegacyRulesetBeatmaps && submissionAvailable)
{
var upload = new EditorMenuItem(EditorStrings.SubmitBeatmap, MenuItemType.Standard, submitBeatmap);
saveRelatedMenuItems.Add(upload);
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);
}
@ -1295,6 +1321,42 @@ namespace osu.Game.Screens.Edit
}
}
private void submitBeatmap()
{
if (api.State.Value != APIState.Online)
{
loginOverlay?.Show();
return;
}
if (!editorBeatmap.HitObjects.Any())
{
notifications?.Post(new SimpleNotification
{
Text = BeatmapSubmissionStrings.EmptyBeatmapsCannotBeSubmitted,
});
return;
}
if (HasUnsavedChanges)
{
dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() =>
{
if (!Save())
return false;
startSubmission();
return true;
})));
}
else
{
startSubmission();
}
void startSubmission() => this.Push(new BeatmapSubmissionScreen());
}
private void exportBeatmap(bool legacy)
{
if (HasUnsavedChanges)

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Localisation;
@ -15,10 +17,14 @@ namespace osu.Game.Screens.Edit.Submission
}
[BackgroundDependencyLoader]
private void load()
private void load(IBindable<WorkingBeatmap> beatmap)
{
AddStep<ScreenContentPermissions>();
AddStep<ScreenFrequentlyAskedQuestions>();
if (beatmap.Value.BeatmapSetInfo.OnlineID <= 0)
{
AddStep<ScreenContentPermissions>();
AddStep<ScreenFrequentlyAskedQuestions>();
}
AddStep<ScreenSubmissionSettings>();
Header.Title = BeatmapSubmissionStrings.BeatmapSubmissionTitle;

View File

@ -0,0 +1,446 @@
// 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.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.IO.Archives;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens.Select;
using osuTK;
namespace osu.Game.Screens.Edit.Submission
{
public partial class BeatmapSubmissionScreen : OsuScreen
{
private BeatmapSubmissionOverlay overlay = null!;
public override bool DisallowExternalBeatmapRulesetChanges => true;
protected override bool InitialBackButtonVisibility => false;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[Resolved]
private RealmAccess realmAccess { get; set; } = null!;
[Resolved]
private Storage storage { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private OsuConfigManager configManager { get; set; } = null!;
[Resolved]
private OsuGame? game { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
[Cached]
private BeatmapSubmissionSettings settings { get; } = new BeatmapSubmissionSettings();
private Container submissionProgress = null!;
private SubmissionStageProgress exportStep = null!;
private SubmissionStageProgress createSetStep = null!;
private SubmissionStageProgress uploadStep = null!;
private SubmissionStageProgress updateStep = null!;
private Container successContainer = null!;
private Container flashLayer = null!;
private uint? beatmapSetId;
private MemoryStream? beatmapPackageStream;
private ProgressNotification? exportProgressNotification;
private ProgressNotification? updateProgressNotification;
private Live<BeatmapSetInfo>? importedSet;
[BackgroundDependencyLoader]
private void load()
{
AddRangeInternal(new Drawable[]
{
overlay = new BeatmapSubmissionOverlay(),
submissionProgress = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
AutoSizeDuration = 400,
AutoSizeEasing = Easing.OutQuint,
Alpha = 0,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.6f,
Masking = true,
CornerRadius = 10,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Padding = new MarginPadding(20),
Spacing = new Vector2(5),
Children = new Drawable[]
{
createSetStep = new SubmissionStageProgress
{
StageDescription = BeatmapSubmissionStrings.Preparing,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
exportStep = new SubmissionStageProgress
{
StageDescription = BeatmapSubmissionStrings.Exporting,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
uploadStep = new SubmissionStageProgress
{
StageDescription = BeatmapSubmissionStrings.Uploading,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
updateStep = new SubmissionStageProgress
{
StageDescription = BeatmapSubmissionStrings.Finishing,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
successContainer = new Container
{
Padding = new MarginPadding(20),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
CornerRadius = BeatmapCard.CORNER_RADIUS,
Child = flashLayer = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = BeatmapCard.CORNER_RADIUS,
Depth = float.MinValue,
Alpha = 0,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
}
}
},
}
}
}
}
});
overlay.State.BindValueChanged(_ =>
{
if (overlay.State.Value == Visibility.Hidden)
{
if (!overlay.Completed)
{
allowExit();
this.Exit();
}
else
{
submissionProgress.FadeIn(200, Easing.OutQuint);
createBeatmapSet();
}
}
});
}
private void createBeatmapSet()
{
bool beatmapHasOnlineId = Beatmap.Value.BeatmapSetInfo.OnlineID > 0;
var createRequest = beatmapHasOnlineId
? PutBeatmapSetRequest.UpdateExisting(
(uint)Beatmap.Value.BeatmapSetInfo.OnlineID,
Beatmap.Value.BeatmapSetInfo.Beatmaps.Where(b => b.OnlineID > 0).Select(b => (uint)b.OnlineID).ToArray(),
(uint)Beatmap.Value.BeatmapSetInfo.Beatmaps.Count(b => b.OnlineID <= 0),
settings.Target.Value)
: PutBeatmapSetRequest.CreateNew((uint)Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, settings.Target.Value);
createRequest.Success += async response =>
{
createSetStep.SetCompleted();
beatmapSetId = response.BeatmapSetId;
// at this point the set has an assigned online ID.
// it's important to proactively store it to the realm database,
// so that in the event in further failures in the process, the online ID is not lost.
// losing it can incur creation of redundant new sets server-side, or even cause online ID confusion.
if (!beatmapHasOnlineId)
{
await realmAccess.WriteAsync(r =>
{
var refetchedSet = r.Find<BeatmapSetInfo>(Beatmap.Value.BeatmapSetInfo.ID);
refetchedSet!.OnlineID = (int)beatmapSetId.Value;
}).ConfigureAwait(true);
}
await createBeatmapPackage(response).ConfigureAwait(true);
};
createRequest.Failure += ex =>
{
createSetStep.SetFailed(ex.Message);
Logger.Log($"Beatmap set submission failed on creation: {ex}");
allowExit();
};
createSetStep.SetInProgress();
api.Queue(createRequest);
}
private async Task createBeatmapPackage(PutBeatmapSetResponse response)
{
Debug.Assert(ThreadSafety.IsUpdateThread);
exportStep.SetInProgress();
try
{
beatmapPackageStream = new MemoryStream();
exportProgressNotification = new ProgressNotification();
var legacyBeatmapExporter = new SubmissionBeatmapExporter(storage, response);
await legacyBeatmapExporter
.ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification)
.ConfigureAwait(true);
}
catch (Exception ex)
{
exportStep.SetFailed(ex.Message);
exportProgressNotification = null;
Logger.Log($"Beatmap set submission failed on export: {ex}");
allowExit();
}
exportStep.SetCompleted();
exportProgressNotification = null;
await Task.Delay(200).ConfigureAwait(true);
if (response.Files.Count > 0)
await patchBeatmapSet(response.Files).ConfigureAwait(true);
else
replaceBeatmapSet();
}
private async Task patchBeatmapSet(ICollection<BeatmapSetFile> onlineFiles)
{
Debug.Assert(beatmapSetId != null);
Debug.Assert(beatmapPackageStream != null);
var onlineFilesByFilename = onlineFiles.ToDictionary(f => f.Filename, f => f.SHA2Hash);
// disposing the `ArchiveReader` makes the underlying stream no longer readable which we don't want.
// make a local copy to defend against it.
using var archiveReader = new ZipArchiveReader(new MemoryStream(beatmapPackageStream.ToArray()));
var filesToUpdate = new HashSet<string>();
foreach (string filename in archiveReader.Filenames)
{
string localHash = archiveReader.GetStream(filename).ComputeSHA2Hash();
if (!onlineFilesByFilename.Remove(filename, out string? onlineHash))
{
filesToUpdate.Add(filename);
continue;
}
if (localHash != onlineHash)
filesToUpdate.Add(filename);
}
var changedFiles = new Dictionary<string, byte[]>();
foreach (string file in filesToUpdate)
changedFiles.Add(file, await archiveReader.GetStream(file).ReadAllBytesToArrayAsync().ConfigureAwait(true));
var patchRequest = new PatchBeatmapPackageRequest(beatmapSetId.Value);
patchRequest.FilesChanged.AddRange(changedFiles);
patchRequest.FilesDeleted.AddRange(onlineFilesByFilename.Keys);
patchRequest.Success += uploadCompleted;
patchRequest.Failure += ex =>
{
uploadStep.SetFailed(ex.Message);
Logger.Log($"Beatmap submission failed on upload: {ex}");
allowExit();
};
patchRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / total);
api.Queue(patchRequest);
uploadStep.SetInProgress();
}
private void replaceBeatmapSet()
{
Debug.Assert(beatmapSetId != null);
Debug.Assert(beatmapPackageStream != null);
var uploadRequest = new ReplaceBeatmapPackageRequest(beatmapSetId.Value, beatmapPackageStream.ToArray());
uploadRequest.Success += uploadCompleted;
uploadRequest.Failure += ex =>
{
uploadStep.SetFailed(ex.Message);
Logger.Log($"Beatmap submission failed on upload: {ex}");
allowExit();
};
uploadRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / Math.Max(total, 1));
api.Queue(uploadRequest);
uploadStep.SetInProgress();
}
private void uploadCompleted()
{
uploadStep.SetCompleted();
updateLocalBeatmap().ConfigureAwait(true);
}
private async Task updateLocalBeatmap()
{
Debug.Assert(beatmapSetId != null);
Debug.Assert(beatmapPackageStream != null);
updateStep.SetInProgress();
await Task.Delay(200).ConfigureAwait(true);
try
{
importedSet = await beatmaps.ImportAsUpdate(
updateProgressNotification = new ProgressNotification(),
new ImportTask(beatmapPackageStream, $"{beatmapSetId}.osz"),
Beatmap.Value.BeatmapSetInfo).ConfigureAwait(true);
}
catch (Exception ex)
{
updateStep.SetFailed(ex.Message);
Logger.Log($"Beatmap submission failed on local update: {ex}");
allowExit();
return;
}
updateStep.SetCompleted();
showBeatmapCard();
allowExit();
if (configManager.Get<bool>(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission))
{
await Task.Delay(1000).ConfigureAwait(true);
game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetId}");
}
}
private void showBeatmapCard()
{
Debug.Assert(beatmapSetId != null);
var getBeatmapSetRequest = new GetBeatmapSetRequest((int)beatmapSetId.Value);
getBeatmapSetRequest.Success += beatmapSet =>
{
LoadComponentAsync(new BeatmapCardExtra(beatmapSet, false), loaded =>
{
successContainer.Add(loaded);
flashLayer.FadeOutFromOne(2000, Easing.OutQuint);
});
};
api.Queue(getBeatmapSetRequest);
}
private void allowExit()
{
BackButtonVisibility.Value = true;
}
protected override void Update()
{
base.Update();
if (exportProgressNotification != null && exportProgressNotification.Ongoing)
exportStep.SetInProgress(exportProgressNotification.Progress);
if (updateProgressNotification != null && updateProgressNotification.Ongoing)
updateStep.SetInProgress(updateProgressNotification.Progress);
}
public override bool OnExiting(ScreenExitEvent e)
{
// We probably want a method of cancelling in the future…
if (!BackButtonVisibility.Value)
return true;
if (importedSet != null)
{
game?.PerformFromScreen(s =>
{
if (s is OsuScreen osuScreen)
{
Debug.Assert(importedSet != null);
var targetBeatmap = importedSet.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == Beatmap.Value.BeatmapInfo.DifficultyName)
?? importedSet.Value.Beatmaps.First();
osuScreen.Beatmap.Value = beatmaps.GetWorkingBeatmap(targetBeatmap);
}
s.Push(new EditorLoader());
}, [typeof(SongSelect)]);
return false;
}
return base.OnExiting(e);
}
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(e);
overlay.Show();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
beatmapPackageStream?.Dispose();
}
}
}

View File

@ -0,0 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Online.API.Requests;
namespace osu.Game.Screens.Edit.Submission
{
public class BeatmapSubmissionSettings
{
public Bindable<BeatmapSubmissionTarget> Target { get; } = new Bindable<BeatmapSubmissionTarget>();
}
}

View File

@ -11,6 +11,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays;
using osuTK;
@ -22,8 +23,10 @@ namespace osu.Game.Screens.Edit.Submission
private readonly BindableBool notifyOnDiscussionReplies = new BindableBool();
private readonly BindableBool loadInBrowserAfterSubmission = new BindableBool();
public override LocalisableString? NextStepText => BeatmapSubmissionStrings.ConfirmSubmission;
[BackgroundDependencyLoader]
private void load(OsuConfigManager configManager, OsuColour colours)
private void load(OsuConfigManager configManager, OsuColour colours, BeatmapSubmissionSettings settings)
{
configManager.BindWith(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, notifyOnDiscussionReplies);
configManager.BindWith(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, loadInBrowserAfterSubmission);
@ -39,6 +42,7 @@ namespace osu.Game.Screens.Edit.Submission
{
RelativeSizeAxes = Axes.X,
Caption = BeatmapSubmissionStrings.BeatmapSubmissionTargetCaption,
Current = settings.Target,
},
new FormCheckBox
{
@ -60,14 +64,5 @@ namespace osu.Game.Screens.Edit.Submission
}
});
}
private enum BeatmapSubmissionTarget
{
[LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetWIP))]
WIP,
[LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetPending))]
Pending,
}
}
}

View File

@ -0,0 +1,53 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Screens.Edit.Submission
{
public class SubmissionBeatmapExporter : LegacyBeatmapExporter
{
private readonly uint? beatmapSetId;
private readonly HashSet<int>? allocatedBeatmapIds;
public SubmissionBeatmapExporter(Storage storage, PutBeatmapSetResponse putBeatmapSetResponse)
: base(storage)
{
beatmapSetId = putBeatmapSetResponse.BeatmapSetId;
allocatedBeatmapIds = putBeatmapSetResponse.BeatmapIds.Select(id => (int)id).ToHashSet();
}
protected override void MutateBeatmap(BeatmapSetInfo beatmapSet, IBeatmap playableBeatmap)
{
base.MutateBeatmap(beatmapSet, playableBeatmap);
if (beatmapSetId != null && allocatedBeatmapIds != null)
{
playableBeatmap.BeatmapInfo.BeatmapSet = beatmapSet;
playableBeatmap.BeatmapInfo.BeatmapSet!.OnlineID = (int)beatmapSetId;
if (allocatedBeatmapIds.Contains(playableBeatmap.BeatmapInfo.OnlineID))
{
allocatedBeatmapIds.Remove(playableBeatmap.BeatmapInfo.OnlineID);
return;
}
if (playableBeatmap.BeatmapInfo.OnlineID > 0)
throw new InvalidOperationException(@"Encountered beatmap with ID that has not been assigned to it by the server!");
if (allocatedBeatmapIds.Count == 0)
throw new InvalidOperationException(@"Ran out of new beatmap IDs to assign to unsubmitted beatmaps!");
int newId = allocatedBeatmapIds.First();
allocatedBeatmapIds.Remove(newId);
playableBeatmap.BeatmapInfo.OnlineID = newId;
}
}
}
}

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

@ -140,6 +140,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
Size = new Vector2(28, 20),
CountryCode = user?.CountryCode ?? default
},
new UpdateableTeamFlag(user?.Team)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(40, 20),
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
@ -161,11 +167,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 +222,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 +254,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

@ -0,0 +1,178 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using Realms;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
public partial class AddPlaylistToCollectionButton : RoundedButton, IHasTooltip
{
private readonly Room room;
private IDisposable? beatmapSubscription;
private IDisposable? collectionSubscription;
private Live<BeatmapCollection>? collection;
private HashSet<string> localBeatmapHashes = new HashSet<string>();
[Resolved]
private RealmAccess realm { get; set; } = null!;
[Resolved(canBeNull: true)]
private INotificationOverlay? notifications { get; set; }
public AddPlaylistToCollectionButton(Room room)
{
this.room = room;
}
[BackgroundDependencyLoader]
private void load()
{
Action = () =>
{
if (room.Playlist.Count == 0)
return;
int countBefore = 0;
int countAfter = 0;
Text = "Updating collection...";
Enabled.Value = false;
realm.WriteAsync(r =>
{
var beatmaps = getBeatmapsForPlaylist(r).ToArray();
var c = getCollectionsForPlaylist(r).FirstOrDefault()
?? r.Add(new BeatmapCollection(room.Name));
countBefore = c.BeatmapMD5Hashes.Count;
foreach (var item in beatmaps)
{
if (!c.BeatmapMD5Hashes.Contains(item.MD5Hash))
c.BeatmapMD5Hashes.Add(item.MD5Hash);
}
countAfter = c.BeatmapMD5Hashes.Count;
}).ContinueWith(_ => Schedule(() =>
{
if (countBefore == 0)
notifications?.Post(new SimpleNotification { Text = $"Created new collection \"{room.Name}\" with {countAfter} beatmaps." });
else
notifications?.Post(new SimpleNotification { Text = $"Added {countAfter - countBefore} beatmaps to collection \"{room.Name}\"." });
}));
};
}
protected override void LoadComplete()
{
base.LoadComplete();
// will be updated via updateButtonState() when ready.
Enabled.Value = false;
if (room.Playlist.Count == 0)
return;
beatmapSubscription = realm.RegisterForNotifications(getBeatmapsForPlaylist, (sender, _) =>
{
localBeatmapHashes = sender.Select(b => b.MD5Hash).ToHashSet();
Schedule(updateButtonState);
});
collectionSubscription = realm.RegisterForNotifications(getCollectionsForPlaylist, (sender, _) =>
{
collection = sender.FirstOrDefault()?.ToLive(realm);
Schedule(updateButtonState);
});
}
private void updateButtonState()
{
int countToAdd = getCountToBeAdded();
if (collection == null)
Text = $"Create new collection with {countToAdd} beatmaps";
else if (hasAllItemsInCollection)
Text = "Collection complete!";
else
Text = $"Add {countToAdd} beatmaps to collection";
Enabled.Value = countToAdd > 0;
}
private int getCountToBeAdded()
{
if (collection == null)
return localBeatmapHashes.Count;
return collection.PerformRead(c =>
{
int count = localBeatmapHashes.Count;
foreach (string hash in localBeatmapHashes)
{
if (c.BeatmapMD5Hashes.Contains(hash))
count--;
}
return count;
});
}
private IQueryable<BeatmapCollection> getCollectionsForPlaylist(Realm r) => r.All<BeatmapCollection>().Where(c => c.Name == room.Name);
private IQueryable<BeatmapInfo> getBeatmapsForPlaylist(Realm r)
{
return r.All<BeatmapInfo>().Filter(string.Join(" OR ", room.Playlist.Select(item => $"(OnlineID == {item.Beatmap.OnlineID})").Distinct()));
}
private bool hasAllItemsInCollection
{
get
{
if (collection == null)
return false;
return room.Playlist.DistinctBy(i => i.Beatmap.OnlineID).Count() ==
collection.PerformRead(c => c.BeatmapMD5Hashes.Count);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
beatmapSubscription?.Dispose();
collectionSubscription?.Dispose();
}
public LocalisableString TooltipText
{
get
{
if (Enabled.Value)
return string.Empty;
if (hasAllItemsInCollection)
return "All beatmaps have been added!";
return "Download some beatmaps first.";
}
}
}
}

View File

@ -146,7 +146,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
new Drawable?[]
{
// Playlist items column
new GridContainer
{
RelativeSizeAxes = Axes.Both,
@ -169,80 +168,84 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
}
}
},
new Drawable[]
{
new AddPlaylistToCollectionButton(Room)
{
Margin = new MarginPadding { Top = 5 },
RelativeSizeAxes = Axes.X,
Size = new Vector2(1, 40)
}
}
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
}
},
// 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 +276,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

@ -0,0 +1,56 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Skinning;
using osu.Game.Users.Drawables;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
public partial class PlayerTeamFlag : CompositeDrawable, ISerialisableDrawable
{
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => false;
private readonly UpdateableTeamFlag flag;
private const float default_size = 40f;
[Resolved]
private GameplayState? gameplayState { get; set; }
[Resolved]
private IAPIProvider api { get; set; } = null!;
private IBindable<APIUser>? apiUser;
public PlayerTeamFlag()
{
Size = new Vector2(default_size, default_size / 2f);
InternalChild = flag = new UpdateableTeamFlag
{
RelativeSizeAxes = Axes.Both,
};
}
[BackgroundDependencyLoader]
private void load()
{
if (gameplayState != null)
flag.Team = gameplayState.Score.ScoreInfo.User.Team;
else
{
apiUser = api.LocalUser.GetBoundCopy();
apiUser.BindValueChanged(u => flag.Team = u.NewValue.Team, true);
}
}
public bool UsesFixedAnchor { get; set; }
}
}

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

@ -339,6 +339,12 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
Origin = Anchor.CentreLeft,
Size = new Vector2(24, 16),
},
new UpdateableTeamFlag(user.Team)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(40, 20),
},
new DateLabel(score.Date)
{
Anchor = Anchor.CentreLeft,

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API;
@ -221,7 +222,15 @@ namespace osu.Game.Tests.Visual.OnlinePlay
: new APIUser
{
Id = id,
Username = $"User {id}"
Username = $"User {id}",
Team = RNG.NextBool()
? new APITeam
{
Name = "Collective Wangs",
ShortName = "WANG",
FlagUrl = "https://assets.ppy.sh/teams/logo/1/wanglogo.jpg",
}
: null,
})
.Where(u => u != null).ToList(),
});

View File

@ -0,0 +1,87 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Users.Drawables
{
/// <summary>
/// A team logo which can update to a new team when needed.
/// </summary>
public partial class UpdateableTeamFlag : ModelBackedDrawable<APITeam?>
{
public APITeam? Team
{
get => Model;
set
{
Model = value;
Invalidate(Invalidation.Presence);
}
}
protected override double LoadDelay => 200;
public UpdateableTeamFlag(APITeam? team = null)
{
Team = team;
Masking = true;
}
protected override Drawable? CreateDrawable(APITeam? team)
{
if (team == null)
return Empty();
return new TeamFlag(team) { RelativeSizeAxes = Axes.Both };
}
// Generally we just want team flags to disappear if the user doesn't have one.
// This also handles fill flow cases and avoids spacing being added for non-displaying flags.
public override bool IsPresent => base.IsPresent && Team != null;
protected override void Update()
{
base.Update();
CornerRadius = DrawHeight / 8;
}
[LongRunningLoad]
public partial class TeamFlag : CompositeDrawable, IHasTooltip
{
private readonly APITeam team;
public LocalisableString TooltipText { get; }
public TeamFlag(APITeam team)
{
this.team = team;
TooltipText = team.Name;
}
[BackgroundDependencyLoader]
private void load(LargeTextureStore textures)
{
InternalChildren = new Drawable[]
{
new HoverClickSounds(),
new Sprite
{
RelativeSizeAxes = Axes.Both,
Texture = textures.Get(team.FlagUrl)
}
};
}
}
}
}

View File

@ -82,9 +82,10 @@ namespace osu.Game.Users
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(6),
Children = new Drawable[]
Children = new[]
{
CreateFlag(),
CreateTeamLogo(),
// supporter icon is being added later
}
}

View File

@ -130,6 +130,11 @@ namespace osu.Game.Users
Action = Action,
};
protected Drawable CreateTeamLogo() => new UpdateableTeamFlag(User.Team)
{
Size = new Vector2(52, 26),
};
public MenuItem[] ContextMenuItems
{
get

View File

@ -147,9 +147,10 @@ namespace osu.Game.Users
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(6),
Children = new Drawable[]
Children = new[]
{
CreateFlag(),
CreateTeamLogo(),
// supporter icon is being added later
}
}

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

@ -11,6 +11,7 @@ using osu.Game;
using osu.Game.Screens;
using osu.Game.Updater;
using osu.Game.Utils;
using osuTK;
using UIKit;
namespace osu.iOS
@ -22,6 +23,8 @@ namespace osu.iOS
public override bool HideUnlicensedContent => true;
protected override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth);
public OsuGameIOS(AppDelegate appDelegate)
{
this.appDelegate = appDelegate;
@ -41,7 +44,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 +63,7 @@ namespace osu.iOS
appDelegate.Orientations = null;
break;
}
}
});
protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier();