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:
commit
ab88ccb081
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
10
.idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml
Normal file
10
.idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml
Normal 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>
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
75
osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs
Normal file
75
osu.Game.Rulesets.Mania/UI/DefaultManiaJudgementPiece.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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).
|
||||
|
@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new osuTK.Vector2(0.5f),
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
20
osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs
Normal file
20
osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
190
osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs
Normal file
190
osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
201
osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs
Normal file
201
osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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:
|
||||
|
@ -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
|
||||
{
|
||||
}
|
||||
|
BIN
osu.Game.Tests/Resources/Archives/modified-argon-20250214.osk
Normal file
BIN
osu.Game.Tests/Resources/Archives/modified-argon-20250214.osk
Normal file
Binary file not shown.
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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(),
|
||||
},
|
||||
|
@ -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;
|
||||
|
@ -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>()
|
||||
|
@ -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",
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
@ -96,7 +96,6 @@ namespace osu.Game.Collections
|
||||
lastCreated = collections[changes.InsertedIndices[0]].ID;
|
||||
|
||||
foreach (int i in changes.NewModifiedIndices)
|
||||
|
||||
{
|
||||
var updatedItem = collections[i];
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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";
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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))]
|
||||
|
@ -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}";
|
||||
}
|
||||
|
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
23
osu.Game/Online/API/Requests/Responses/APITeam.cs
Normal file
23
osu.Game/Online/API/Requests/Responses/APITeam.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -13,6 +13,7 @@ namespace osu.Game.Online
|
||||
SpectatorUrl = $@"{APIUrl}/signalr/spectator";
|
||||
MultiplayerUrl = $@"{APIUrl}/signalr/multiplayer";
|
||||
MetadataUrl = $@"{APIUrl}/signalr/metadata";
|
||||
BeatmapSubmissionServiceUrl = $@"{APIUrl}/beatmap-submission";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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[]
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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[]
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
446
osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs
Normal file
446
osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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>();
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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[]
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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()
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
56
osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs
Normal file
56
osu.Game/Screens/Play/HUD/PlayerTeamFlag.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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(),
|
||||
});
|
||||
|
87
osu.Game/Users/Drawables/UpdateableTeamFlag.cs
Normal file
87
osu.Game/Users/Drawables/UpdateableTeamFlag.cs
Normal 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)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user