1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 11:20:04 +08:00

Merge branch 'master' of https://github.com/ppy/osu into mania-hidden-configuration

This commit is contained in:
Dylan Nantz 2023-01-06 11:39:50 -05:00
commit 28bd005c21
246 changed files with 3219 additions and 1186 deletions

View File

@ -1,6 +1,6 @@
clone_depth: 1
version: '{build}'
image: Visual Studio 2019
image: Visual Studio 2022
test: off
skip_non_tags: true
configuration: Release
@ -83,4 +83,4 @@ artifacts:
deploy:
- provider: Environment
name: nuget
name: nuget

View File

@ -10,10 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.1219.0" />
</ItemGroup>
<ItemGroup>
<LinkDescription Include="$(MSBuildThisFileDirectory)\osu.Android\Linker.xml"/>
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.1226.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" package="sh.ppy.osulazer" android:installLocation="auto" android:versionName="0.1.0">
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="sh.ppy.osulazer" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!" android:icon="@drawable/lazer" />
</manifest>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<linker>
<assembly fullname="mscorlib">
<!-- see https://github.com/ppy/osu/issues/21516 -->
<type fullname="System.Globalization.*Calendar"/>
</assembly>
</linker>

View File

@ -8,6 +8,9 @@
<UseMauiEssentials>true</UseMauiEssentials>
<!-- This currently causes random lockups during gameplay. https://github.com/mono/mono/issues/18973 -->
<EnableLLVM>false</EnableLLVM>
<Version>0.0.0</Version>
<ApplicationVersion Condition=" '$(ApplicationVersion)' == '' ">1</ApplicationVersion>
<ApplicationDisplayVersion Condition=" '$(ApplicationDisplayVersion)' == '' ">$(Version)</ApplicationDisplayVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj" />

View File

@ -18,6 +18,36 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
{
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
[Test]
public void TestAlwaysHidden()
{
CreateModTest(new ModTestData
{
Mod = new CatchModNoScope
{
HiddenComboCount = { Value = 0 },
},
Autoplay = true,
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Fruit
{
X = CatchPlayfield.CENTER_X * 0.5f,
StartTime = 1000,
},
new Fruit
{
X = CatchPlayfield.CENTER_X * 1.5f,
StartTime = 2000,
}
}
}
});
}
[Test]
public void TestVisibleDuringBreak()
{

View File

@ -26,6 +26,9 @@ namespace osu.Game.Rulesets.Catch.Mods
var catchPlayfield = (CatchPlayfield)playfield;
bool shouldAlwaysShowCatcher = IsBreakTime.Value;
float targetAlpha = shouldAlwaysShowCatcher ? 1 : ComboBasedAlpha;
// AlwaysPresent required for catcher to still act on input when fully hidden.
catchPlayfield.CatcherArea.AlwaysPresent = true;
catchPlayfield.CatcherArea.Alpha = (float)Interpolation.Lerp(catchPlayfield.CatcherArea.Alpha, targetAlpha, Math.Clamp(catchPlayfield.Time.Elapsed / TRANSITION_DURATION, 0, 1));
}
}

View File

@ -1,193 +0,0 @@
// 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;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Skinning.Argon
{
public partial class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
{
protected readonly HitResult Result;
protected SpriteText JudgementText { get; private set; } = null!;
private RingExplosion? ringExplosion;
[Resolved]
private OsuColour colours { get; set; } = null!;
public ArgonJudgementPiece(HitResult result)
{
Result = result;
Origin = Anchor.Centre;
Y = 160;
}
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
JudgementText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = Result.GetDescription().ToUpperInvariant(),
Colour = colours.ForHitResult(Result),
Blending = BlendingParameters.Additive,
Spacing = new Vector2(10, 0),
Font = OsuFont.Default.With(size: 28, weight: FontWeight.Regular),
},
};
if (Result.IsHit())
{
AddInternal(ringExplosion = new RingExplosion(Result)
{
Colour = colours.ForHitResult(Result),
});
}
}
/// <summary>
/// Plays the default animation for this judgement piece.
/// </summary>
/// <remarks>
/// The base implementation only handles fade (for all result types) and misses.
/// Individual rulesets are recommended to implement their appropriate hit animations.
/// </remarks>
public virtual void PlayAnimation()
{
switch (Result)
{
default:
JudgementText
.ScaleTo(Vector2.One)
.ScaleTo(new Vector2(1.4f), 1800, Easing.OutQuint);
break;
case HitResult.Miss:
this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
this.MoveTo(Vector2.Zero);
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
this.RotateTo(0);
this.RotateTo(40, 800, Easing.InQuint);
break;
}
this.FadeOutFromOne(800);
ringExplosion?.PlayAnimation();
}
public Drawable? GetAboveHitObjectsProxiedContent() => null;
private partial class RingExplosion : CompositeDrawable
{
private readonly float travel = 52;
public RingExplosion(HitResult result)
{
const float thickness = 4;
const float small_size = 9;
const float large_size = 14;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Blending = BlendingParameters.Additive;
int countSmall = 0;
int countLarge = 0;
switch (result)
{
case HitResult.Meh:
countSmall = 3;
travel *= 0.3f;
break;
case HitResult.Ok:
case HitResult.Good:
countSmall = 4;
travel *= 0.6f;
break;
case HitResult.Great:
case HitResult.Perfect:
countSmall = 4;
countLarge = 4;
break;
}
for (int i = 0; i < countSmall; i++)
AddInternal(new RingPiece(thickness) { Size = new Vector2(small_size) });
for (int i = 0; i < countLarge; i++)
AddInternal(new RingPiece(thickness) { Size = new Vector2(large_size) });
}
public void PlayAnimation()
{
foreach (var c in InternalChildren)
{
const float start_position_ratio = 0.3f;
float direction = RNG.NextSingle(0, 360);
float distance = RNG.NextSingle(travel / 2, travel);
c.MoveTo(new Vector2(
MathF.Cos(direction) * distance * start_position_ratio,
MathF.Sin(direction) * distance * start_position_ratio
));
c.MoveTo(new Vector2(
MathF.Cos(direction) * distance,
MathF.Sin(direction) * distance
), 600, Easing.OutQuint);
}
this.FadeOutFromOne(1000, Easing.OutQuint);
}
public partial class RingPiece : CircularContainer
{
public RingPiece(float thickness = 9)
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Masking = true;
BorderThickness = thickness;
BorderColour = Color4.White;
Child = new Box
{
AlwaysPresent = true,
Alpha = 0,
RelativeSizeAxes = Axes.Both
};
}
}
}
}
}

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -32,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
foreach (var state in Enum.GetValues(typeof(CatcherAnimationState)).Cast<CatcherAnimationState>())
foreach (var state in Enum.GetValues<CatcherAnimationState>())
{
AddInternal(drawables[state] = getDrawableFor(state).With(d =>
{

View File

@ -11,7 +11,6 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.UI
{
@ -106,41 +105,17 @@ namespace osu.Game.Rulesets.Catch.UI
return false;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
return updateAction(e.Button, getTouchCatchActionFromInput(e.ScreenSpaceMousePosition));
}
protected override bool OnTouchDown(TouchDownEvent e)
{
return updateAction(e.Touch.Source, getTouchCatchActionFromInput(e.ScreenSpaceTouch.Position));
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
Show();
TouchCatchAction? action = getTouchCatchActionFromInput(e.ScreenSpaceMousePosition);
// multiple mouse buttons may be pressed and handling the same action.
foreach (MouseButton button in e.PressedButtons)
updateAction(button, action);
return false;
}
protected override void OnTouchMove(TouchMoveEvent e)
{
updateAction(e.Touch.Source, getTouchCatchActionFromInput(e.ScreenSpaceTouch.Position));
base.OnTouchMove(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
updateAction(e.Button, null);
base.OnMouseUp(e);
}
protected override void OnTouchUp(TouchUpEvent e)
{
updateAction(e.Touch.Source, null);

View File

@ -35,8 +35,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
protected PatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(hitObject, beatmap, previousPattern)
{
if (random == null) throw new ArgumentNullException(nameof(random));
if (originalBeatmap == null) throw new ArgumentNullException(nameof(originalBeatmap));
ArgumentNullException.ThrowIfNull(random);
ArgumentNullException.ThrowIfNull(originalBeatmap);
Random = random;
OriginalBeatmap = originalBeatmap;

View File

@ -33,9 +33,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
protected PatternGenerator(HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern)
{
if (hitObject == null) throw new ArgumentNullException(nameof(hitObject));
if (beatmap == null) throw new ArgumentNullException(nameof(beatmap));
if (previousPattern == null) throw new ArgumentNullException(nameof(previousPattern));
ArgumentNullException.ThrowIfNull(hitObject);
ArgumentNullException.ThrowIfNull(beatmap);
ArgumentNullException.ThrowIfNull(previousPattern);
HitObject = hitObject;
Beatmap = beatmap;

View File

@ -22,8 +22,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils
public static void Sort(T[] keys, IComparer<T> comparer)
{
if (keys == null)
throw new ArgumentNullException(nameof(keys));
ArgumentNullException.ThrowIfNull(keys);
if (keys.Length == 0)
return;

View File

@ -3,7 +3,6 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -18,20 +17,18 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
public partial class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
{
protected readonly HitResult Result;
protected SpriteText JudgementText { get; private set; } = null!;
private RingExplosion? ringExplosion;
[Resolved]
private OsuColour colours { get; set; } = null!;
public ArgonJudgementPiece(HitResult result)
: base(result)
{
Result = result;
AutoSizeAxes = Axes.Both;
Origin = Anchor.Centre;
Y = 160;
}
@ -39,22 +36,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
JudgementText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = Result.GetDescription().ToUpperInvariant(),
Colour = colours.ForHitResult(Result),
Blending = BlendingParameters.Additive,
Spacing = new Vector2(10, 0),
Font = OsuFont.Default.With(size: 28, weight: FontWeight.Regular),
},
};
if (Result.IsHit())
{
AddInternal(ringExplosion = new RingExplosion(Result)
@ -64,6 +45,16 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
}
}
protected override SpriteText CreateJudgementText() =>
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Blending = BlendingParameters.Additive,
Spacing = new Vector2(10, 0),
Font = OsuFont.Default.With(size: 28, weight: FontWeight.Regular),
};
/// <summary>
/// Plays the default animation for this judgement piece.
/// </summary>

View File

@ -27,6 +27,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
switch (lookup)
{
case GameplaySkinComponentLookup<HitResult> resultComponent:
// This should eventually be moved to a skin setting, when supported.
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
return Drawable.Empty();
return new ArgonJudgementPiece(resultComponent.Component);
case ManiaSkinComponentLookup maniaComponent:

View File

@ -134,6 +134,9 @@ namespace osu.Game.Rulesets.Mania.UI
protected override void Dispose(bool isDisposing)
{
// must happen before children are disposed in base call to prevent illegal accesses to the hit explosion pool.
NewResult -= OnNewResult;
base.Dispose(isDisposing);
if (skin != null)
@ -206,18 +209,6 @@ namespace osu.Game.Rulesets.Mania.UI
keyBindingContainer = maniaInputManager?.KeyBindingContainer;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
keyBindingContainer?.TriggerPressed(column.Action.Value);
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
keyBindingContainer?.TriggerReleased(column.Action.Value);
base.OnMouseUp(e);
}
protected override bool OnTouchDown(TouchDownEvent e)
{
keyBindingContainer?.TriggerPressed(column.Action.Value);

View File

@ -29,8 +29,7 @@ namespace osu.Game.Rulesets.Mania.UI
public ManiaPlayfield(List<StageDefinition> stageDefinitions)
{
if (stageDefinitions == null)
throw new ArgumentNullException(nameof(stageDefinitions));
ArgumentNullException.ThrowIfNull(stageDefinitions);
if (stageDefinitions.Count <= 0)
throw new ArgumentException("Can't have zero or fewer stages.");

View File

@ -156,6 +156,9 @@ namespace osu.Game.Rulesets.Mania.UI
protected override void Dispose(bool isDisposing)
{
// must happen before children are disposed in base call to prevent illegal accesses to the judgement pool.
NewResult -= OnNewResult;
base.Dispose(isDisposing);
if (currentSkin != null)

View File

@ -160,9 +160,9 @@ namespace osu.Game.Rulesets.Osu.Tests
static bool assertSamples(HitObject hitObject) => hitObject.Samples.All(s => s.Name != HitSampleInfo.HIT_CLAP && s.Name != HitSampleInfo.HIT_WHISTLE);
}
private Drawable testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats);
private Drawable testSimpleBig(int repeats = 0) => createSlider(repeats: repeats);
private Drawable testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(2, repeats: repeats, stackHeight: 10);
private Drawable testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(repeats: repeats, stackHeight: 10);
private Drawable testDistanceOverflow(int repeats = 0)
{

View File

@ -84,8 +84,8 @@ namespace osu.Game.Rulesets.Osu.Replays
{
public int Compare(ReplayFrame? f1, ReplayFrame? f2)
{
if (f1 == null) throw new ArgumentNullException(nameof(f1));
if (f2 == null) throw new ArgumentNullException(nameof(f2));
ArgumentNullException.ThrowIfNull(f1);
ArgumentNullException.ThrowIfNull(f2);
return f1.Time.CompareTo(f2.Time);
}

View File

@ -1,35 +1,64 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public partial class ArgonFollowCircle : FollowCircle
{
private readonly CircularContainer circleContainer;
private readonly Box circleFill;
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
[Resolved(canBeNull: true)]
private DrawableHitObject? parentObject { get; set; }
public ArgonFollowCircle()
{
InternalChild = new CircularContainer
InternalChild = circleContainer = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 4,
BorderColour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
Blending = BlendingParameters.Additive,
Child = new Box
Child = circleFill = new Box
{
Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
RelativeSizeAxes = Axes.Both,
Alpha = 0.3f,
}
};
}
[BackgroundDependencyLoader]
private void load()
{
if (parentObject != null)
accentColour.BindTo(parentObject.AccentColour);
}
protected override void LoadComplete()
{
base.LoadComplete();
accentColour.BindValueChanged(colour =>
{
circleContainer.BorderColour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.5f));
circleFill.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.5f));
}, true);
}
protected override void OnSliderPress()
{
const float duration = 300f;

View File

@ -3,7 +3,6 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@ -17,42 +16,24 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public partial class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
{
protected readonly HitResult Result;
protected SpriteText JudgementText { get; private set; } = null!;
private RingExplosion? ringExplosion;
[Resolved]
private OsuColour colours { get; set; } = null!;
public ArgonJudgementPiece(HitResult result)
: base(result)
{
Result = result;
AutoSizeAxes = Axes.Both;
Origin = Anchor.Centre;
}
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
JudgementText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = Result.GetDescription().ToUpperInvariant(),
Colour = colours.ForHitResult(Result),
Blending = BlendingParameters.Additive,
Spacing = new Vector2(5, 0),
Font = OsuFont.Default.With(size: 20, weight: FontWeight.Bold),
},
};
if (Result.IsHit())
{
AddInternal(ringExplosion = new RingExplosion(Result)
@ -62,6 +43,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
}
}
protected override SpriteText CreateJudgementText() =>
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Blending = BlendingParameters.Additive,
Spacing = new Vector2(5, 0),
Font = OsuFont.Default.With(size: 20, weight: FontWeight.Bold),
};
/// <summary>
/// Plays the default animation for this judgement piece.
/// </summary>

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@ -21,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
private readonly Vector2 defaultIconScale = new Vector2(0.6f, 0.8f);
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
[Resolved(canBeNull: true)]
private DrawableHitObject? parentObject { get; set; }
@ -37,7 +41,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
fill = new Box
{
Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -53,10 +56,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
};
}
[BackgroundDependencyLoader]
private void load()
{
if (parentObject != null)
accentColour.BindTo(parentObject.AccentColour);
}
protected override void LoadComplete()
{
base.LoadComplete();
accentColour.BindValueChanged(colour =>
{
fill.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.5f));
}, true);
if (parentObject != null)
{
parentObject.ApplyCustomUpdateState += updateStateTransforms;

View File

@ -19,6 +19,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
switch (lookup)
{
case GameplaySkinComponentLookup<HitResult> resultComponent:
// This should eventually be moved to a skin setting, when supported.
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
return Drawable.Empty();
return new ArgonJudgementPiece(resultComponent.Component);
case OsuSkinComponentLookup osuComponent:

View File

@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.UI
HitPolicy = new StartTimeOrderedHitPolicy();
var hitWindows = new OsuHitWindows();
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
foreach (var result in Enum.GetValues<HitResult>().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgementLoaded));
AddRangeInternal(poolDictionary.Values);

View File

@ -3,7 +3,6 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -18,20 +17,16 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{
public partial class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
{
protected readonly HitResult Result;
protected SpriteText JudgementText { get; private set; } = null!;
private RingExplosion? ringExplosion;
[Resolved]
private OsuColour colours { get; set; } = null!;
public ArgonJudgementPiece(HitResult result)
: base(result)
{
Result = result;
RelativePositionAxes = Axes.Both;
RelativeSizeAxes = Axes.Both;
}
@ -39,21 +34,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
JudgementText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = Result.GetDescription().ToUpperInvariant(),
Colour = colours.ForHitResult(Result),
Blending = BlendingParameters.Additive,
Spacing = new Vector2(10, 0),
RelativePositionAxes = Axes.Both,
Font = OsuFont.Default.With(size: 20, weight: FontWeight.Regular),
},
};
if (Result.IsHit())
{
AddInternal(ringExplosion = new RingExplosion(Result)
@ -64,6 +44,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
}
}
protected override SpriteText CreateJudgementText() =>
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Blending = BlendingParameters.Additive,
Spacing = new Vector2(10, 0),
RelativePositionAxes = Axes.Both,
Font = OsuFont.Default.With(size: 20, weight: FontWeight.Regular),
};
/// <summary>
/// Plays the default animation for this judgement piece.
/// </summary>

View File

@ -19,6 +19,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
switch (component)
{
case GameplaySkinComponentLookup<HitResult> resultComponent:
// This should eventually be moved to a skin setting, when supported.
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
return Drawable.Empty();
return new ArgonJudgementPiece(resultComponent.Component);
case TaikoSkinComponentLookup taikoComponent:

View File

@ -107,24 +107,6 @@ namespace osu.Game.Rulesets.Taiko.UI
return false;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (!validMouse(e))
return false;
handleDown(e.Button, e.ScreenSpaceMousePosition);
return true;
}
protected override void OnMouseUp(MouseUpEvent e)
{
if (!validMouse(e))
return;
handleUp(e.Button);
base.OnMouseUp(e);
}
protected override bool OnTouchDown(TouchDownEvent e)
{
handleDown(e.Touch.Source, e.ScreenSpaceTouchDownPosition);

View File

@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Taiko.UI
var hitWindows = new TaikoHitWindows();
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r)))
foreach (var result in Enum.GetValues<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r)))
{
judgementPools.Add(result, new DrawablePool<DrawableTaikoJudgement>(15));
explosionPools.Add(result, new HitExplosionPool(result));

View File

@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Judgements;
@ -139,6 +140,29 @@ namespace osu.Game.Tests.Gameplay
Assert.That(score.MaximumStatistics[HitResult.LargeBonus], Is.EqualTo(1));
}
[Test]
public void TestAccuracyModes()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = Enumerable.Range(0, 4).Select(_ => new TestHitObject(HitResult.Great)).ToList<HitObject>()
};
var scoreProcessor = new ScoreProcessor(new OsuRuleset());
scoreProcessor.ApplyBeatmap(beatmap);
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1));
Assert.That(scoreProcessor.MinimumAccuracy.Value, Is.EqualTo(0));
Assert.That(scoreProcessor.MaximumAccuracy.Value, Is.EqualTo(1));
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].CreateJudgement()) { Type = HitResult.Ok });
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].CreateJudgement()) { Type = HitResult.Great });
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo((double)(100 + 300) / (2 * 300)).Within(Precision.DOUBLE_EPSILON));
Assert.That(scoreProcessor.MinimumAccuracy.Value, Is.EqualTo((double)(100 + 300) / (4 * 300)).Within(Precision.DOUBLE_EPSILON));
Assert.That(scoreProcessor.MaximumAccuracy.Value, Is.EqualTo((double)(100 + 3 * 300) / (4 * 300)).Within(Precision.DOUBLE_EPSILON));
}
private class TestJudgement : Judgement
{
public override HitResult MaxResult { get; }

View File

@ -60,6 +60,6 @@ namespace osu.Game.Tests.Mods
/// This local helper is used rather than <see cref="Ruleset.CreateAllMods"/>, because the aforementioned method flattens multi mods.
/// </remarks>>
private static IEnumerable<MultiMod> getMultiMods(Ruleset ruleset)
=> Enum.GetValues(typeof(ModType)).Cast<ModType>().SelectMany(ruleset.GetModsFor).OfType<MultiMod>();
=> Enum.GetValues<ModType>().SelectMany(ruleset.GetModsFor).OfType<MultiMod>();
}
}

View File

@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Editing
{
public partial class TestSceneZoomableScrollContainer : OsuManualInputManagerTestScene
{
private ZoomableScrollContainer scrollContainer;
private TestZoomableScrollContainer scrollContainer;
private Drawable innerBox;
[SetUpSteps]
@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Editing
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(30)
},
scrollContainer = new ZoomableScrollContainer(1, 60, 1)
scrollContainer = new TestZoomableScrollContainer(1, 60, 1)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -93,6 +93,14 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Inner container width matches scroll container", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
}
[Test]
public void TestWidthUpdatesOnSecondZoomSetup()
{
AddAssert("Inner container width = 1x", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
AddStep("reload zoom", () => scrollContainer.SetupZoom(10, 10, 60));
AddAssert("Inner container width = 10x", () => innerBox.DrawWidth == scrollContainer.DrawWidth * 10);
}
[Test]
public void TestZoom0()
{
@ -190,5 +198,15 @@ namespace osu.Game.Tests.Visual.Editing
private Quad scrollQuad => scrollContainer.ScreenSpaceDrawQuad;
private Quad boxQuad => innerBox.ScreenSpaceDrawQuad;
private partial class TestZoomableScrollContainer : ZoomableScrollContainer
{
public TestZoomableScrollContainer(int minimum, float maximum, float initial)
: base(minimum, maximum, initial)
{
}
public new void SetupZoom(float initial, float minimum, float maximum) => base.SetupZoom(initial, minimum, maximum);
}
}
}

View File

@ -257,7 +257,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
prepareTestAPI(true);
createPlayerTest(false, createRuleset: () => new OsuRuleset
createPlayerTest(createRuleset: () => new OsuRuleset
{
RulesetInfo =
{

View File

@ -201,6 +201,23 @@ namespace osu.Game.Tests.Visual.Menus
AddAssert("volume not changed", () => Audio.Volume.Value == 0.5);
}
[Test]
public void TestRulesetSelectorOverflow()
{
AddStep("set toolbar width", () =>
{
toolbar.RelativeSizeAxes = Axes.None;
toolbar.Width = 400;
});
AddStep("move mouse over news toggle button", () =>
{
var button = toolbar.ChildrenOfType<ToolbarNewsButton>().Single();
InputManager.MoveMouseTo(button);
});
AddAssert("no ruleset toggle buttons hovered", () => !toolbar.ChildrenOfType<ToolbarRulesetTabButton>().Any(button => button.IsHovered));
AddUntilStep("toolbar gradient visible", () => toolbar.ChildrenOfType<Toolbar.ToolbarBackground>().Single().Children.All(d => d.Alpha > 0));
}
public partial class TestToolbar : Toolbar
{
public new Bindable<OverlayActivation> OverlayActivationMode => base.OverlayActivationMode as Bindable<OverlayActivation>;

View File

@ -3,6 +3,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@ -46,10 +47,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("item removed", () => !playlist.Items.Contains(selectedItem));
}
[Test]
public void TestNextItemSelectedAfterDeletion()
[TestCase(true)]
[TestCase(false)]
public void TestNextItemSelectedAfterDeletion(bool allowSelection)
{
createPlaylist();
createPlaylist(p =>
{
p.AllowSelection = allowSelection;
});
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
@ -57,7 +62,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
moveToDeleteButton(0);
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
AddAssert("item 0 is " + (allowSelection ? "selected" : "not selected"), () => playlist.SelectedItem.Value == (allowSelection ? playlist.Items[0] : null));
}
[Test]
@ -117,7 +122,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.MoveMouseTo(item.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(0), offset);
});
private void createPlaylist()
private void createPlaylist(Action<TestPlaylist> setupPlaylist = null)
{
AddStep("create playlist", () =>
{
@ -154,6 +159,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
}
setupPlaylist?.Invoke(playlist);
});
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));

View File

@ -54,6 +54,8 @@ namespace osu.Game.Tests.Visual.Online
{
overlay.ShowBeatmapSet(new APIBeatmapSet
{
Genre = new BeatmapSetOnlineGenre { Id = 15, Name = "Future genre" },
Language = new BeatmapSetOnlineLanguage { Id = 15, Name = "Future language" },
OnlineID = 1235,
Title = @"an awesome beatmap",
Artist = @"naru narusegawa",

View File

@ -530,6 +530,52 @@ namespace osu.Game.Tests.Visual.Online
});
}
[Test]
public void TestTextBoxSavePerChannel()
{
var testPMChannel = new Channel(testUser);
AddStep("show overlay", () => chatOverlay.Show());
joinTestChannel(0);
joinChannel(testPMChannel);
AddAssert("listing is visible", () => listingIsVisible);
AddStep("search for 'number 2'", () => chatOverlayTextBox.Text = "number 2");
AddAssert("'number 2' saved to selector", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == "number 2");
AddStep("select normal channel", () => clickDrawable(getChannelListItem(testChannel1)));
AddAssert("text box cleared on normal channel", () => chatOverlayTextBox.Text == string.Empty);
AddAssert("nothing saved on normal channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == string.Empty);
AddStep("type '727'", () => chatOverlayTextBox.Text = "727");
AddAssert("'727' saved to normal channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == "727");
AddStep("select PM channel", () => clickDrawable(getChannelListItem(testPMChannel)));
AddAssert("text box cleared on PM channel", () => chatOverlayTextBox.Text == string.Empty);
AddAssert("nothing saved on PM channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == string.Empty);
AddStep("type 'hello'", () => chatOverlayTextBox.Text = "hello");
AddAssert("'hello' saved to PM channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == "hello");
AddStep("select normal channel", () => clickDrawable(getChannelListItem(testChannel1)));
AddAssert("text box contains '727'", () => chatOverlayTextBox.Text == "727");
AddStep("select PM channel", () => clickDrawable(getChannelListItem(testPMChannel)));
AddAssert("text box contains 'hello'", () => chatOverlayTextBox.Text == "hello");
AddStep("click close button", () =>
{
ChannelListItemCloseButton closeButton = getChannelListItem(testPMChannel).ChildrenOfType<ChannelListItemCloseButton>().Single();
clickDrawable(closeButton);
});
AddAssert("listing is visible", () => listingIsVisible);
AddAssert("text box contains 'channel 2'", () => chatOverlayTextBox.Text == "number 2");
AddUntilStep("only channel 2 visible", () =>
{
IEnumerable<ChannelListingItem> listingItems = chatOverlay.ChildrenOfType<ChannelListingItem>()
.Where(item => item.IsPresent);
return listingItems.Count() == 1 && listingItems.Single().Channel == testChannel2;
});
}
private void joinTestChannel(int i)
{
AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i]));

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Overlays.Profile.Sections.Kudosu;
using System.Collections.Generic;
using System;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Graphics;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Rulesets.Catch;
@ -24,7 +22,7 @@ namespace osu.Game.Tests.Visual.Online
public TestSceneProfileRulesetSelector()
{
ProfileRulesetSelector selector;
var user = new Bindable<APIUser>();
var user = new Bindable<APIUser?>();
Child = selector = new ProfileRulesetSelector
{

View File

@ -0,0 +1,302 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Models;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Solo;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
{
[HeadlessTest]
public partial class TestSceneSoloStatisticsWatcher : OsuTestScene
{
protected override bool UseOnlineAPI => false;
private SoloStatisticsWatcher watcher = null!;
[Resolved]
private SpectatorClient spectatorClient { get; set; } = null!;
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
private Action<GetUsersRequest>? handleGetUsersRequest;
private Action<GetUserRequest>? handleGetUserRequest;
private IDisposable? subscription;
private readonly Dictionary<(int userId, string rulesetName), UserStatistics> serverSideStatistics = new Dictionary<(int userId, string rulesetName), UserStatistics>();
[SetUpSteps]
public void SetUpSteps()
{
AddStep("clear server-side stats", () => serverSideStatistics.Clear());
AddStep("set up request handling", () =>
{
handleGetUserRequest = null;
handleGetUsersRequest = null;
dummyAPI.HandleRequest = request =>
{
switch (request)
{
case GetUsersRequest getUsersRequest:
if (handleGetUsersRequest != null)
{
handleGetUsersRequest?.Invoke(getUsersRequest);
}
else
{
int userId = getUsersRequest.UserIds.Single();
var response = new GetUsersResponse
{
Users = new List<APIUser>
{
new APIUser
{
Id = userId,
RulesetsStatistics = new Dictionary<string, UserStatistics>
{
["osu"] = tryGetStatistics(userId, "osu"),
["taiko"] = tryGetStatistics(userId, "taiko"),
["fruits"] = tryGetStatistics(userId, "fruits"),
["mania"] = tryGetStatistics(userId, "mania"),
}
}
}
};
getUsersRequest.TriggerSuccess(response);
}
return true;
case GetUserRequest getUserRequest:
if (handleGetUserRequest != null)
{
handleGetUserRequest.Invoke(getUserRequest);
}
else
{
int userId = int.Parse(getUserRequest.Lookup);
string rulesetName = getUserRequest.Ruleset.ShortName;
var response = new APIUser
{
Id = userId,
Statistics = tryGetStatistics(userId, rulesetName)
};
getUserRequest.TriggerSuccess(response);
}
return true;
default:
return false;
}
};
});
AddStep("create watcher", () =>
{
Child = watcher = new SoloStatisticsWatcher();
});
}
private UserStatistics tryGetStatistics(int userId, string rulesetName)
=> serverSideStatistics.TryGetValue((userId, rulesetName), out var stats) ? stats : new UserStatistics();
[Test]
public void TestStatisticsUpdateFiredAfterRegistrationAddedAndScoreProcessed()
{
int userId = getUserId();
long scoreId = getScoreId();
setUpUser(userId);
var ruleset = new OsuRuleset().RulesetInfo;
SoloStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
AddUntilStep("update received", () => update != null);
AddAssert("values before are correct", () => update!.Before.TotalScore, () => Is.EqualTo(4_000_000));
AddAssert("values after are correct", () => update!.After.TotalScore, () => Is.EqualTo(5_000_000));
}
[Test]
public void TestStatisticsUpdateFiredAfterScoreProcessedAndRegistrationAdded()
{
int userId = getUserId();
setUpUser(userId);
long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo;
// note ordering - in this test processing completes *before* the registration is added.
feignScoreProcessing(userId, ruleset, 5_000_000);
SoloStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
AddUntilStep("update received", () => update != null);
AddAssert("values before are correct", () => update!.Before.TotalScore, () => Is.EqualTo(4_000_000));
AddAssert("values after are correct", () => update!.After.TotalScore, () => Is.EqualTo(5_000_000));
}
[Test]
public void TestStatisticsUpdateNotFiredIfUserLoggedOut()
{
int userId = getUserId();
setUpUser(userId);
long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo;
SoloStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000);
AddStep("log out user", () => dummyAPI.Logout());
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
AddWaitStep("wait a bit", 5);
AddAssert("update not received", () => update == null);
AddStep("log in user", () => dummyAPI.Login("user", "password"));
}
[Test]
public void TestStatisticsUpdateNotFiredIfAnotherUserLoggedIn()
{
int userId = getUserId();
setUpUser(userId);
long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo;
SoloStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000);
AddStep("change user", () => dummyAPI.LocalUser.Value = new APIUser { Id = getUserId() });
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
AddWaitStep("wait a bit", 5);
AddAssert("update not received", () => update == null);
}
[Test]
public void TestStatisticsUpdateNotFiredIfScoreIdDoesNotMatch()
{
int userId = getUserId();
setUpUser(userId);
long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo;
SoloStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000);
AddStep("signal another score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, getScoreId()));
AddWaitStep("wait a bit", 5);
AddAssert("update not received", () => update == null);
}
// the behaviour exercised in this test may not be final, it is mostly assumed for simplicity.
// in the long run we may want each score's update to be entirely isolated from others, rather than have prior unobserved updates merge into the latest.
[Test]
public void TestIgnoredScoreUpdateIsMergedIntoNextOne()
{
int userId = getUserId();
setUpUser(userId);
long firstScoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo;
feignScoreProcessing(userId, ruleset, 5_000_000);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, firstScoreId));
long secondScoreId = getScoreId();
feignScoreProcessing(userId, ruleset, 6_000_000);
SoloStatisticsUpdate? update = null;
registerForUpdates(secondScoreId, ruleset, receivedUpdate => update = receivedUpdate);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, secondScoreId));
AddUntilStep("update received", () => update != null);
AddAssert("values before are correct", () => update!.Before.TotalScore, () => Is.EqualTo(4_000_000));
AddAssert("values after are correct", () => update!.After.TotalScore, () => Is.EqualTo(6_000_000));
}
[Test]
public void TestStatisticsUpdateNotFiredAfterSubscriptionDisposal()
{
int userId = getUserId();
setUpUser(userId);
long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo;
SoloStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
AddStep("unsubscribe", () => subscription!.Dispose());
feignScoreProcessing(userId, ruleset, 5_000_000);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
AddWaitStep("wait a bit", 5);
AddAssert("update not received", () => update == null);
}
private int nextUserId = 2000;
private long nextScoreId = 50000;
private int getUserId() => ++nextUserId;
private long getScoreId() => ++nextScoreId;
private void setUpUser(int userId)
{
AddStep("fetch initial stats", () =>
{
serverSideStatistics[(userId, "osu")] = new UserStatistics { TotalScore = 4_000_000 };
serverSideStatistics[(userId, "taiko")] = new UserStatistics { TotalScore = 3_000_000 };
serverSideStatistics[(userId, "fruits")] = new UserStatistics { TotalScore = 2_000_000 };
serverSideStatistics[(userId, "mania")] = new UserStatistics { TotalScore = 1_000_000 };
dummyAPI.LocalUser.Value = new APIUser { Id = userId };
});
}
private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action<SoloStatisticsUpdate> onUpdateReady) =>
AddStep("register for updates", () => subscription = watcher.RegisterForStatisticsUpdateAfter(
new ScoreInfo(Beatmap.Value.BeatmapInfo, new OsuRuleset().RulesetInfo, new RealmUser())
{
Ruleset = rulesetInfo,
OnlineID = scoreId
},
onUpdateReady));
private void feignScoreProcessing(int userId, RulesetInfo rulesetInfo, long newTotalScore)
=> AddStep("feign score processing", () => serverSideStatistics[(userId, rulesetInfo.ShortName)] = new UserStatistics { TotalScore = newTotalScore });
}
}

View File

@ -50,6 +50,8 @@ namespace osu.Game.Tests.Visual.Online
private ChannelManager channelManager;
private TestStandAloneChatDisplay chatDisplay;
private TestStandAloneChatDisplay chatWithTextBox;
private TestStandAloneChatDisplay chatWithTextBox2;
private int messageIdSequence;
private Channel testChannel;
@ -78,7 +80,7 @@ namespace osu.Game.Tests.Visual.Online
private void reinitialiseDrawableDisplay()
{
Children = new[]
Children = new Drawable[]
{
chatDisplay = new TestStandAloneChatDisplay
{
@ -88,13 +90,28 @@ namespace osu.Game.Tests.Visual.Online
Size = new Vector2(400, 80),
Channel = { Value = testChannel },
},
new TestStandAloneChatDisplay(true)
new FillFlowContainer
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Margin = new MarginPadding(20),
Size = new Vector2(400, 150),
Channel = { Value = testChannel },
Children = new[]
{
chatWithTextBox = new TestStandAloneChatDisplay(true)
{
Margin = new MarginPadding(20),
Size = new Vector2(400, 150),
Channel = { Value = testChannel },
},
chatWithTextBox2 = new TestStandAloneChatDisplay(true)
{
Margin = new MarginPadding(20),
Size = new Vector2(400, 150),
Channel = { Value = testChannel },
},
}
}
};
}
@ -351,6 +368,13 @@ namespace osu.Game.Tests.Visual.Online
checkScrolledToBottom();
}
[Test]
public void TestTextBoxSync()
{
AddStep("type 'hello' to text box 1", () => chatWithTextBox.ChildrenOfType<StandAloneChatDisplay.ChatTextBox>().Single().Text = "hello");
AddAssert("text box 2 contains 'hello'", () => chatWithTextBox2.ChildrenOfType<StandAloneChatDisplay.ChatTextBox>().Single().Text == "hello");
}
private void fillChat(int count = 10)
{
AddStep("fill chat", () =>

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Linq;
using NUnit.Framework;
@ -20,7 +18,7 @@ namespace osu.Game.Tests.Visual.Online
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
private ProfileHeader header;
private ProfileHeader header = null!;
[SetUpSteps]
public void SetUpSteps()

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Linq;
using NUnit.Framework;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using NUnit.Framework;
using osu.Framework.Graphics;
@ -14,7 +12,7 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture]
public partial class TestSceneUserProfilePreviousUsernames : OsuTestScene
{
private PreviousUsernames container;
private PreviousUsernames container = null!;
[SetUp]
public void SetUp() => Schedule(() =>
@ -50,7 +48,7 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("Is hidden", () => container.Alpha == 0);
}
private static readonly APIUser[] users =
private static readonly APIUser?[] users =
{
new APIUser { Id = 1, PreviousUsernames = new[] { "username1" } },
new APIUser { Id = 2, PreviousUsernames = new[] { "longusername", "longerusername" } },

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;

View File

@ -16,13 +16,17 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers.Markdown;
using osu.Game.Graphics.Containers.Markdown.Footnotes;
using osu.Game.Overlays;
using osu.Game.Overlays.Wiki.Markdown;
using osu.Game.Users.Drawables;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Online
{
public partial class TestSceneWikiMarkdownContainer : OsuTestScene
public partial class TestSceneWikiMarkdownContainer : OsuManualInputManagerTestScene
{
private OverlayScrollContainer scrollContainer;
private TestMarkdownContainer markdownContainer;
[Cached]
@ -38,15 +42,25 @@ namespace osu.Game.Tests.Visual.Online
Colour = overlayColour.Background5,
RelativeSizeAxes = Axes.Both,
},
new BasicScrollContainer
scrollContainer = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(20),
Child = markdownContainer = new TestMarkdownContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
}
};
scrollContainer.Child = new DependencyProvidingContainer
{
CachedDependencies = new (Type, object)[]
{
(typeof(OverlayScrollContainer), scrollContainer)
},
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = markdownContainer = new TestMarkdownContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
};
});
@ -197,6 +211,63 @@ Line after image";
markdownContainer.CurrentPath = @"https://dev.ppy.sh";
markdownContainer.Text = "::{flag=\"AU\"}:: ::{flag=\"ZZ\"}::";
});
AddAssert("Two flags visible", () => markdownContainer.ChildrenOfType<DrawableFlag>().Count(), () => Is.EqualTo(2));
}
[Test]
public void TestHeadingWithIdAttribute()
{
AddStep("Add heading with ID", () =>
{
markdownContainer.Text = "# This is a heading with an ID {#this-is-the-id}";
});
AddAssert("ID not visible", () => markdownContainer.ChildrenOfType<SpriteText>().All(spriteText => spriteText.Text != "{#this-is-the-id}"));
}
[Test]
public void TestFootnotes()
{
AddStep("set content", () => markdownContainer.Text = @"This text has a footnote[^test].
Here's some more text[^test2] with another footnote!
# Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam efficitur laoreet posuere. Ut accumsan tortor in ipsum tincidunt ultrices. Suspendisse a malesuada tellus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce a sagittis nibh. In et velit sit amet mauris aliquet consectetur quis vehicula lorem. Etiam sit amet tellus ac velit ornare maximus. Donec quis metus eget libero ullamcorper imperdiet id vitae arcu. Vivamus iaculis rhoncus purus malesuada mollis. Vestibulum dictum at nisi sed tincidunt. Suspendisse finibus, ipsum ut dapibus commodo, leo eros porttitor sapien, non scelerisque nisi ligula sed ex. Pellentesque magna orci, hendrerit eu iaculis sit amet, ullamcorper in urna. Vivamus dictum mauris orci, nec facilisis dolor fringilla eu. Sed at porttitor nisi, at venenatis urna. Ut at orci vitae libero semper ullamcorper eu ut risus. Mauris hendrerit varius enim, ut varius nisi feugiat mattis.
## In at eros urna. Sed ipsum lorem, tempor sit amet purus in, vehicula pellentesque leo. Fusce volutpat pellentesque velit sit amet porttitor. Nulla eget erat ex. Praesent eu lacinia est, quis vehicula lacus. Donec consequat ultrices neque, at finibus quam efficitur vel. Vestibulum molestie nisl sit amet metus semper, at vestibulum massa rhoncus. Quisque imperdiet suscipit augue, et dignissim odio eleifend ut.
Aliquam sed vestibulum mauris, ut lobortis elit. Sed quis lacinia erat. Nam ultricies, risus non pellentesque sollicitudin, mauris dolor tincidunt neque, ac porta ipsum dui quis libero. Integer eget velit neque. Vestibulum venenatis mauris vitae rutrum vestibulum. Maecenas suscipit eu purus eu tempus. Nam dui nisl, bibendum condimentum mollis et, gravida vel dui. Sed et eros rutrum, facilisis sapien eu, mattis ligula. Fusce finibus pulvinar dolor quis consequat.
Donec ipsum felis, feugiat vel fermentum at, commodo eu sapien. Suspendisse nec enim vitae felis laoreet laoreet. Phasellus purus quam, fermentum a pharetra vel, tempor et urna. Integer vitae quam diam. Aliquam tincidunt tortor a iaculis convallis. Suspendisse potenti. Cras quis risus quam. Nullam tincidunt in lorem posuere sagittis.
Phasellus eu nunc nec ligula semper fringilla. Aliquam magna neque, placerat sed urna tristique, laoreet pharetra nulla. Vivamus maximus turpis purus, eu viverra dolor sodales porttitor. Praesent bibendum sapien purus, sed ultricies dolor iaculis sed. Fusce congue hendrerit malesuada. Nulla nulla est, auctor ac fringilla sed, ornare a lorem. Donec quis velit imperdiet, imperdiet sem non, pellentesque sapien. Maecenas in orci id ipsum placerat facilisis non sed nisi. Duis dictum lorem sodales odio dictum eleifend. Vestibulum bibendum euismod quam, eget pharetra orci facilisis sed. Vivamus at diam non ipsum consequat tristique. Pellentesque gravida dignissim pellentesque. Donec ullamcorper lacinia orci, id consequat purus faucibus quis. Phasellus metus nunc, iaculis a interdum vel, congue sed erat. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam eros libero, hendrerit luctus nulla vitae, luctus maximus nunc.
[^test]: This is a **footnote**.
[^test2]: This is another footnote [with a link](https://google.com/)!");
AddStep("shrink scroll height", () => scrollContainer.Height = 0.5f);
AddStep("press second footnote link", () =>
{
InputManager.MoveMouseTo(markdownContainer.ChildrenOfType<OsuMarkdownFootnoteLink>().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
AddUntilStep("second footnote scrolled into view", () =>
{
var footnote = markdownContainer.ChildrenOfType<OsuMarkdownFootnote>().ElementAt(1);
return scrollContainer.ScreenSpaceDrawQuad.Contains(footnote.ScreenSpaceDrawQuad.TopLeft)
&& scrollContainer.ScreenSpaceDrawQuad.Contains(footnote.ScreenSpaceDrawQuad.BottomRight);
});
AddStep("press first footnote backlink", () =>
{
InputManager.MoveMouseTo(markdownContainer.ChildrenOfType<OsuMarkdownFootnoteBacklink>().First());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("first footnote link scrolled into view", () =>
{
var footnote = markdownContainer.ChildrenOfType<OsuMarkdownFootnoteLink>().First();
return scrollContainer.ScreenSpaceDrawQuad.Contains(footnote.ScreenSpaceDrawQuad.TopLeft)
&& scrollContainer.ScreenSpaceDrawQuad.Contains(footnote.ScreenSpaceDrawQuad.BottomRight);
});
}
private partial class TestMarkdownContainer : WikiMarkdownContainer

View File

@ -4,8 +4,11 @@
#nullable disable
using System;
using System.Linq;
using System.Net;
using NUnit.Framework;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@ -29,6 +32,15 @@ namespace osu.Game.Tests.Visual.Online
AddStep("Show main page", () => wiki.Show());
}
[Test]
public void TestCancellationDoesntShowError()
{
AddStep("Show main page", () => wiki.Show());
AddStep("Show another page", () => wiki.ShowPage("Article_styling_criteria/Formatting"));
AddUntilStep("Current path is not error", () => wiki.CurrentPath != "error");
}
[Test]
public void TestArticlePage()
{
@ -56,7 +68,9 @@ namespace osu.Game.Tests.Visual.Online
public void TestErrorPage()
{
setUpWikiResponse(responseArticlePage);
AddStep("Show Error Page", () => wiki.ShowPage("Error"));
AddStep("Show nonexistent page", () => wiki.ShowPage("This_page_will_error_out"));
AddUntilStep("Wait for error page", () => wiki.CurrentPath == "error");
AddUntilStep("Error message correct", () => wiki.ChildrenOfType<SpriteText>().Any(text => text.Text == "\"This_page_will_error_out\"."));
}
private void setUpWikiResponse(APIWikiPage r, string redirectionPath = null)

View File

@ -0,0 +1,117 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online.Solo;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics.User;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Ranking
{
public partial class TestSceneOverallRanking : OsuTestScene
{
private OverallRanking overallRanking = null!;
[Test]
public void TestUpdatePending()
{
createDisplay();
}
[Test]
public void TestAllIncreased()
{
createDisplay();
displayUpdate(
new UserStatistics
{
GlobalRank = 12_345,
Accuracy = 98.99,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = 5_072
},
new UserStatistics
{
GlobalRank = 1_234,
Accuracy = 99.07,
MaxCombo = 2_352,
RankedScore = 23_124_231_435,
TotalScore = 123_124_231_435,
PP = 5_434
});
}
[Test]
public void TestAllDecreased()
{
createDisplay();
displayUpdate(
new UserStatistics
{
GlobalRank = 1_234,
Accuracy = 99.07,
MaxCombo = 2_352,
RankedScore = 23_124_231_435,
TotalScore = 123_124_231_435,
PP = 5_434
},
new UserStatistics
{
GlobalRank = 12_345,
Accuracy = 98.99,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = 5_072
});
}
[Test]
public void TestNoChanges()
{
var statistics = new UserStatistics
{
GlobalRank = 12_345,
Accuracy = 98.99,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = 5_072
};
createDisplay();
displayUpdate(statistics, statistics);
}
[Test]
public void TestNotRanked()
{
var statistics = new UserStatistics
{
GlobalRank = null,
Accuracy = 98.99,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = null
};
createDisplay();
displayUpdate(statistics, statistics);
}
private void createDisplay() => AddStep("create display", () => Child = overallRanking = new OverallRanking
{
Width = 400,
Anchor = Anchor.Centre,
Origin = Anchor.Centre
});
private void displayUpdate(UserStatistics before, UserStatistics after) =>
AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new SoloStatisticsUpdate(new ScoreInfo(), before, after));
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using NUnit.Framework;
using osu.Game.Overlays.Profile.Sections;
using osu.Framework.Testing;
@ -18,7 +16,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
private ProfileSubsectionHeader header;
private ProfileSubsectionHeader header = null!;
[Test]
public void TestHiddenCounter()

View File

@ -55,6 +55,16 @@ namespace osu.Game.Tests.Visual.UserInterface
};
});
[Test]
public void TestDisplay()
{
AddRepeatStep("toggle expanded state", () =>
{
InputManager.MoveMouseTo(group.ChildrenOfType<IconButton>().Single());
InputManager.Click(MouseButton.Left);
}, 5);
}
[Test]
public void TestClickExpandButtonMultipleTimes()
{

View File

@ -32,7 +32,7 @@ namespace osu.Game.Tournament.Components
public TournamentBeatmapPanel(TournamentBeatmap beatmap, string mod = null)
{
if (beatmap == null) throw new ArgumentNullException(nameof(beatmap));
ArgumentNullException.ThrowIfNull(beatmap);
Beatmap = beatmap;
this.mod = mod;

View File

@ -127,7 +127,7 @@ namespace osu.Game.Tournament.IPC
using (var stream = IPCStorage.GetStream(file_ipc_state_filename))
using (var sr = new StreamReader(stream))
{
State.Value = (TourneyState)Enum.Parse(typeof(TourneyState), sr.ReadLine().AsNonNull());
State.Value = Enum.Parse<TourneyState>(sr.ReadLine().AsNonNull());
}
}
catch (Exception)

View File

@ -45,7 +45,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
continue;
// ReSharper disable once PossibleNullReferenceException
string[] split = line.Split(':');
string[] split = line.Split(':', StringSplitOptions.TrimEntries);
if (split.Length < 2)
{
@ -55,9 +55,9 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
teams.Add(new TournamentTeam
{
FullName = { Value = split[1].Trim(), },
Acronym = { Value = split.Length >= 3 ? split[2].Trim() : null, },
FlagName = { Value = split[0].Trim() }
FullName = { Value = split[1], },
Acronym = { Value = split.Length >= 3 ? split[2] : null, },
FlagName = { Value = split[0] }
});
}
}

View File

@ -43,7 +43,7 @@ namespace osu.Game.Tournament.Screens.Editors
{
var countries = new List<TournamentTeam>();
foreach (var country in Enum.GetValues(typeof(CountryCode)).Cast<CountryCode>().Skip(1))
foreach (var country in Enum.GetValues<CountryCode>().Skip(1))
{
countries.Add(new TournamentTeam
{

View File

@ -211,8 +211,7 @@ namespace osu.Game.Beatmaps.ControlPoints
public static T BinarySearch<T>(IReadOnlyList<T> list, double time)
where T : class, IControlPoint
{
if (list == null)
throw new ArgumentNullException(nameof(list));
ArgumentNullException.ThrowIfNull(list);
if (list.Count == 0)
return null;

View File

@ -15,8 +15,7 @@ namespace osu.Game.Beatmaps.Drawables
public BeatmapBackgroundSprite(IWorkingBeatmap working)
{
if (working == null)
throw new ArgumentNullException(nameof(working));
ArgumentNullException.ThrowIfNull(working);
this.working = working;
}

View File

@ -138,9 +138,18 @@ namespace osu.Game.Beatmaps.Drawables.Cards
// This avoids depth issues where a hovered (scaled) card to the right of another card would be beneath the card to the left.
this.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint);
background.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
dropdownContent.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
borderContainer.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
if (Expanded.Value)
{
background.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
dropdownContent.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
borderContainer.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
}
else
{
background.FadeOut(BeatmapCard.TRANSITION_DURATION / 3f, Easing.OutQuint);
dropdownContent.FadeOut(BeatmapCard.TRANSITION_DURATION / 3f, Easing.OutQuint);
borderContainer.FadeOut(BeatmapCard.TRANSITION_DURATION / 3f, Easing.OutQuint);
}
content.TweenEdgeEffectTo(new EdgeEffectParameters
{

View File

@ -18,8 +18,7 @@ namespace osu.Game.Beatmaps.Drawables
public OnlineBeatmapSetCover(IBeatmapSetOnlineInfo set, BeatmapSetCoverType type = BeatmapSetCoverType.Cover)
{
if (set == null)
throw new ArgumentNullException(nameof(set));
ArgumentNullException.ThrowIfNull(set);
this.set = set;
this.type = type;

View File

@ -57,8 +57,7 @@ namespace osu.Game.Beatmaps.Formats
public static Decoder<T> GetDecoder<T>(LineBufferedReader stream)
where T : new()
{
if (stream == null)
throw new ArgumentNullException(nameof(stream));
ArgumentNullException.ThrowIfNull(stream);
if (!decoders.TryGetValue(typeof(T), out var typedDecoders))
throw new IOException(@"Unknown decoder type");

View File

@ -160,7 +160,7 @@ namespace osu.Game.Beatmaps.Formats
break;
case @"SampleSet":
defaultSampleBank = (LegacySampleBank)Enum.Parse(typeof(LegacySampleBank), pair.Value);
defaultSampleBank = Enum.Parse<LegacySampleBank>(pair.Value);
break;
case @"SampleVolume":
@ -218,7 +218,7 @@ namespace osu.Game.Beatmaps.Formats
break;
case @"Countdown":
beatmap.BeatmapInfo.Countdown = (CountdownType)Enum.Parse(typeof(CountdownType), pair.Value);
beatmap.BeatmapInfo.Countdown = Enum.Parse<CountdownType>(pair.Value);
break;
case @"CountdownOffset":

View File

@ -300,7 +300,7 @@ namespace osu.Game.Beatmaps.Formats
{
var comboColour = colours[i];
writer.Write(FormattableString.Invariant($"Combo{i}: "));
writer.Write(FormattableString.Invariant($"Combo{1 + i}: "));
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.R * byte.MaxValue)},"));
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.G * byte.MaxValue)},"));
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.B * byte.MaxValue)},"));

View File

@ -132,13 +132,7 @@ namespace osu.Game.Beatmaps.Formats
protected KeyValuePair<string, string> SplitKeyVal(string line, char separator = ':', bool shouldTrim = true)
{
string[] split = line.Split(separator, 2);
if (shouldTrim)
{
for (int i = 0; i < split.Length; i++)
split[i] = split[i].Trim();
}
string[] split = line.Split(separator, 2, shouldTrim ? StringSplitOptions.TrimEntries : StringSplitOptions.None);
return new KeyValuePair<string, string>
(

View File

@ -301,11 +301,11 @@ namespace osu.Game.Beatmaps.Formats
}
}
private string parseLayer(string value) => Enum.Parse(typeof(LegacyStoryLayer), value).ToString();
private string parseLayer(string value) => Enum.Parse<LegacyStoryLayer>(value).ToString();
private Anchor parseOrigin(string value)
{
var origin = (LegacyOrigins)Enum.Parse(typeof(LegacyOrigins), value);
var origin = Enum.Parse<LegacyOrigins>(value);
switch (origin)
{
@ -343,8 +343,8 @@ namespace osu.Game.Beatmaps.Formats
private AnimationLoopType parseAnimationLoopType(string value)
{
var parsed = (AnimationLoopType)Enum.Parse(typeof(AnimationLoopType), value);
return Enum.IsDefined(typeof(AnimationLoopType), parsed) ? parsed : AnimationLoopType.LoopForever;
var parsed = Enum.Parse<AnimationLoopType>(value);
return Enum.IsDefined(parsed) ? parsed : AnimationLoopType.LoopForever;
}
private void handleVariables(string line)

View File

@ -34,8 +34,6 @@ namespace osu.Game.Beatmaps
// TODO: remove once the fallback lookup is not required (and access via `working.BeatmapInfo.Metadata` directly).
public BeatmapMetadata Metadata => BeatmapInfo.Metadata;
public Waveform Waveform => waveform.Value;
public Storyboard Storyboard => storyboard.Value;
public Texture Background => GetBackground(); // Texture uses ref counting, so we want to return a new instance every usage.
@ -48,10 +46,11 @@ namespace osu.Game.Beatmaps
private readonly object beatmapFetchLock = new object();
private readonly Lazy<Waveform> waveform;
private readonly Lazy<Storyboard> storyboard;
private readonly Lazy<ISkin> skin;
private Track track; // track is not Lazy as we allow transferring and loading multiple times.
private Waveform waveform; // waveform is also not Lazy as the track may change.
protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager)
{
@ -60,7 +59,6 @@ namespace osu.Game.Beatmaps
BeatmapInfo = beatmapInfo;
BeatmapSetInfo = beatmapInfo.BeatmapSet ?? new BeatmapSetInfo();
waveform = new Lazy<Waveform>(GetWaveform);
storyboard = new Lazy<Storyboard>(GetStoryboard);
skin = new Lazy<ISkin>(GetSkin);
}
@ -108,7 +106,16 @@ namespace osu.Game.Beatmaps
public virtual bool TrackLoaded => track != null;
public Track LoadTrack() => track = GetBeatmapTrack() ?? GetVirtualTrack(1000);
public Track LoadTrack()
{
track = GetBeatmapTrack() ?? GetVirtualTrack(1000);
// the track may have changed, recycle the current waveform.
waveform?.Dispose();
waveform = null;
return track;
}
public void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0)
{
@ -171,6 +178,12 @@ namespace osu.Game.Beatmaps
#endregion
#region Waveform
public Waveform Waveform => waveform ??= GetWaveform();
#endregion
#region Beatmap
public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;

View File

@ -4,7 +4,6 @@
using System;
using System.IO;
using System.Linq;
using osu.Framework;
using osu.Framework.Extensions;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
@ -63,10 +62,10 @@ namespace osu.Game.Database
private void copyToStore(RealmFile file, Stream data, bool preferHardLinks)
{
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows && data is FileStream fs && preferHardLinks)
if (data is FileStream fs && preferHardLinks)
{
// attempt to do a fast hard link rather than copy.
if (HardLinkHelper.CreateHardLink(Storage.GetFullPath(file.GetStoragePath(), true), fs.Name, IntPtr.Zero))
if (HardLinkHelper.TryCreateHardLink(Storage.GetFullPath(file.GetStoragePath(), true), fs.Name))
return;
}

View File

@ -36,8 +36,7 @@ namespace osu.Game.Graphics.Containers
/// <param name="easing">The easing type of the initial transform.</param>
public void StartTracking(OsuLogo logo, double duration = 0, Easing easing = Easing.None)
{
if (logo == null)
throw new ArgumentNullException(nameof(logo));
ArgumentNullException.ThrowIfNull(logo);
if (logo.IsTracking && Logo == null)
throw new InvalidOperationException($"Cannot track an instance of {typeof(OsuLogo)} to multiple {typeof(LogoTrackingContainer)}s");

View File

@ -0,0 +1,34 @@
// 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 Markdig;
using Markdig.Extensions.GenericAttributes;
using Markdig.Renderers;
using Markdig.Syntax;
namespace osu.Game.Graphics.Containers.Markdown.Extensions
{
/// <summary>
/// A variant of <see cref="Markdig.Extensions.GenericAttributes.GenericAttributesExtension"/>
/// which only handles generic attributes in the current markdown <see cref="Block"/> and ignores inline generic attributes.
/// </summary>
/// <remarks>
/// For rationale, see implementation of <see cref="Setup(Markdig.MarkdownPipelineBuilder)"/>.
/// </remarks>
public class BlockAttributeExtension : IMarkdownExtension
{
private readonly GenericAttributesExtension genericAttributesExtension = new GenericAttributesExtension();
public void Setup(MarkdownPipelineBuilder pipeline)
{
genericAttributesExtension.Setup(pipeline);
// GenericAttributesExtension registers a GenericAttributesParser in pipeline.InlineParsers.
// this conflicts with the CustomContainerExtension, leading to some custom containers (e.g. flags) not displaying.
// as a workaround, remove the inline parser here before it can do damage.
pipeline.InlineParsers.RemoveAll(parser => parser is GenericAttributesParser);
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) => genericAttributesExtension.Setup(pipeline, renderer);
}
}

View File

@ -0,0 +1,21 @@
// 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 Markdig;
namespace osu.Game.Graphics.Containers.Markdown.Extensions
{
public static class OsuMarkdownExtensions
{
/// <summary>
/// Uses the block attributes extension.
/// </summary>
/// <param name="pipeline">The pipeline.</param>
/// <returns>The modified pipeline.</returns>
public static MarkdownPipelineBuilder UseBlockAttributes(this MarkdownPipelineBuilder pipeline)
{
pipeline.Extensions.AddIfNotAlready<BlockAttributeExtension>();
return pipeline;
}
}
}

View File

@ -0,0 +1,30 @@
// 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 Markdig.Extensions.Footnotes;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Containers.Markdown.Footnotes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
namespace osu.Game.Graphics.Containers.Markdown.Footnotes
{
public partial class OsuMarkdownFootnote : MarkdownFootnote
{
public OsuMarkdownFootnote(Footnote footnote)
: base(footnote)
{
}
public override SpriteText CreateOrderMarker(int order) => CreateSpriteText().With(marker =>
{
marker.Text = LocalisableString.Format("{0}.", order);
});
public override MarkdownTextFlowContainer CreateTextFlow() => base.CreateTextFlow().With(textFlow =>
{
textFlow.Margin = new MarginPadding { Left = 30 };
});
}
}

View File

@ -0,0 +1,62 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using Markdig.Extensions.Footnotes;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Graphics.Containers.Markdown.Footnotes
{
public partial class OsuMarkdownFootnoteBacklink : OsuHoverContainer
{
private readonly FootnoteLink backlink;
private SpriteIcon spriteIcon = null!;
[Resolved]
private IMarkdownTextComponent parentTextComponent { get; set; } = null!;
protected override IEnumerable<Drawable> EffectTargets => spriteIcon.Yield();
public OsuMarkdownFootnoteBacklink(FootnoteLink backlink)
{
this.backlink = backlink;
}
[BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider colourProvider, OsuMarkdownContainer markdownContainer, OverlayScrollContainer? scrollContainer)
{
float fontSize = parentTextComponent.CreateSpriteText().Font.Size;
Size = new Vector2(fontSize);
IdleColour = colourProvider.Light2;
HoverColour = colourProvider.Light1;
Add(spriteIcon = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Left = 5 },
Size = new Vector2(fontSize / 2),
Icon = FontAwesome.Solid.ArrowUp,
});
if (scrollContainer != null)
{
Action = () =>
{
var footnoteLink = markdownContainer.ChildrenOfType<OsuMarkdownFootnoteLink>().Single(footnoteLink => footnoteLink.FootnoteLink.Index == backlink.Index);
scrollContainer.ScrollIntoView(footnoteLink);
};
}
}
}
}

View File

@ -0,0 +1,80 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using Markdig.Extensions.Footnotes;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers.Markdown.Footnotes
{
public partial class OsuMarkdownFootnoteLink : OsuHoverContainer, IHasCustomTooltip
{
public readonly FootnoteLink FootnoteLink;
private SpriteText spriteText = null!;
[Resolved]
private IMarkdownTextComponent parentTextComponent { get; set; } = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[Resolved]
private OsuMarkdownContainer markdownContainer { get; set; } = null!;
protected override IEnumerable<Drawable> EffectTargets => spriteText.Yield();
public OsuMarkdownFootnoteLink(FootnoteLink footnoteLink)
{
FootnoteLink = footnoteLink;
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader(true)]
private void load(OsuMarkdownContainer markdownContainer, OverlayScrollContainer? scrollContainer)
{
IdleColour = colourProvider.Light2;
HoverColour = colourProvider.Light1;
spriteText = parentTextComponent.CreateSpriteText();
Add(spriteText.With(t =>
{
float baseSize = t.Font.Size;
t.Font = t.Font.With(size: baseSize * 0.58f);
t.Margin = new MarginPadding { Bottom = 0.33f * baseSize };
t.Text = LocalisableString.Format("[{0}]", FootnoteLink.Index);
}));
if (scrollContainer != null)
{
Action = () =>
{
var footnote = markdownContainer.ChildrenOfType<OsuMarkdownFootnote>().Single(footnote => footnote.Footnote.Label == FootnoteLink.Footnote.Label);
scrollContainer.ScrollIntoView(footnote);
};
}
}
public object TooltipContent
{
get
{
var span = FootnoteLink.Footnote.LastChild.Span;
return markdownContainer.Text.Substring(span.Start, span.Length);
}
}
public ITooltip GetCustomTooltip() => new OsuMarkdownFootnoteTooltip(colourProvider);
}
}

View File

@ -0,0 +1,76 @@
// 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 Markdig.Extensions.Footnotes;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Graphics.Containers.Markdown.Footnotes
{
public partial class OsuMarkdownFootnoteTooltip : CompositeDrawable, ITooltip
{
private readonly FootnoteMarkdownContainer markdownContainer;
[Cached]
private OverlayColourProvider colourProvider;
public OsuMarkdownFootnoteTooltip(OverlayColourProvider colourProvider)
{
this.colourProvider = colourProvider;
Masking = true;
Width = 200;
AutoSizeAxes = Axes.Y;
CornerRadius = 4;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background6
},
markdownContainer = new FootnoteMarkdownContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
DocumentMargin = new MarginPadding(),
DocumentPadding = new MarginPadding { Horizontal = 10, Vertical = 5 }
}
};
}
public void Move(Vector2 pos) => Position = pos;
public void SetContent(object content) => markdownContainer.SetContent((string)content);
private partial class FootnoteMarkdownContainer : OsuMarkdownContainer
{
private string? lastFootnote;
public void SetContent(string footnote)
{
if (footnote == lastFootnote)
return;
lastFootnote = Text = footnote;
}
public override MarkdownTextFlowContainer CreateTextFlow() => new FootnoteMarkdownTextFlowContainer();
}
private partial class FootnoteMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer
{
protected override void AddFootnoteBacklink(FootnoteLink footnoteBacklink)
{
// we don't want footnote backlinks to show up in tooltips.
}
}
}
}

View File

@ -4,41 +4,25 @@
#nullable disable
using Markdig;
using Markdig.Extensions.AutoLinks;
using Markdig.Extensions.CustomContainers;
using Markdig.Extensions.EmphasisExtras;
using Markdig.Extensions.Footnotes;
using Markdig.Extensions.Tables;
using Markdig.Extensions.Yaml;
using Markdig.Syntax;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Containers.Markdown.Footnotes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Containers.Markdown.Footnotes;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Graphics.Containers.Markdown
{
[Cached]
public partial class OsuMarkdownContainer : MarkdownContainer
{
/// <summary>
/// Allows this markdown container to parse and link footnotes.
/// </summary>
/// <seealso cref="FootnoteExtension"/>
protected virtual bool Footnotes => false;
/// <summary>
/// Allows this markdown container to make URL text clickable.
/// </summary>
/// <seealso cref="AutoLinkExtension"/>
protected virtual bool Autolinks => false;
/// <summary>
/// Allows this markdown container to parse custom containers (used for flags and infoboxes).
/// </summary>
/// <seealso cref="CustomContainerExtension"/>
protected virtual bool CustomContainers => false;
public OsuMarkdownContainer()
{
LineSpacing = 21;
@ -99,25 +83,17 @@ namespace osu.Game.Graphics.Containers.Markdown
return new OsuMarkdownUnorderedListItem(level);
}
// reference: https://github.com/ppy/osu-web/blob/05488a96b25b5a09f2d97c54c06dd2bae59d1dc8/app/Libraries/Markdown/OsuMarkdown.php#L301
protected override MarkdownPipeline CreateBuilder()
{
var pipeline = new MarkdownPipelineBuilder()
.UseAutoIdentifiers()
.UsePipeTables()
.UseEmphasisExtras(EmphasisExtraOptions.Strikethrough)
.UseYamlFrontMatter();
protected override MarkdownFootnoteGroup CreateFootnoteGroup(FootnoteGroup footnoteGroup) => base.CreateFootnoteGroup(footnoteGroup).With(g => g.Spacing = new Vector2(5));
if (Footnotes)
pipeline = pipeline.UseFootnotes();
protected override MarkdownFootnote CreateFootnote(Footnote footnote) => new OsuMarkdownFootnote(footnote);
if (Autolinks)
pipeline = pipeline.UseAutoLinks();
protected sealed override MarkdownPipeline CreateBuilder()
=> Options.BuildPipeline();
if (CustomContainers)
pipeline.UseCustomContainers();
return pipeline.Build();
}
/// <summary>
/// Creates a <see cref="OsuMarkdownContainerOptions"/> instance which is used to determine
/// which CommonMark/Markdig extensions should be enabled for this <see cref="OsuMarkdownContainer"/>.
/// </summary>
protected virtual OsuMarkdownContainerOptions Options => new OsuMarkdownContainerOptions();
}
}

View File

@ -0,0 +1,71 @@
// 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 Markdig;
using Markdig.Extensions.AutoLinks;
using Markdig.Extensions.CustomContainers;
using Markdig.Extensions.EmphasisExtras;
using Markdig.Extensions.Footnotes;
using osu.Game.Graphics.Containers.Markdown.Extensions;
namespace osu.Game.Graphics.Containers.Markdown
{
/// <summary>
/// Groups options of customising the set of available extensions to <see cref="OsuMarkdownContainer"/> instances.
/// </summary>
public class OsuMarkdownContainerOptions
{
/// <summary>
/// Allows the <see cref="OsuMarkdownContainer"/> to parse and link footnotes.
/// </summary>
/// <seealso cref="FootnoteExtension"/>
public bool Footnotes { get; init; }
/// <summary>
/// Allows the <see cref="OsuMarkdownContainer"/> container to make URL text clickable.
/// </summary>
/// <seealso cref="AutoLinkExtension"/>
public bool Autolinks { get; init; }
/// <summary>
/// Allows the <see cref="OsuMarkdownContainer"/> to parse custom containers (used for flags and infoboxes).
/// </summary>
/// <seealso cref="CustomContainerExtension"/>
public bool CustomContainers { get; init; }
/// <summary>
/// Allows the <see cref="OsuMarkdownContainer"/> to parse custom attributes in block elements (used e.g. for custom anchor names in the wiki).
/// </summary>
/// <seealso cref="BlockAttributeExtension"/>
public bool BlockAttributes { get; init; }
/// <summary>
/// Returns a prepared <see cref="MarkdownPipeline"/> according to the options specified by the current <see cref="OsuMarkdownContainerOptions"/> instance.
/// </summary>
/// <remarks>
/// Compare: https://github.com/ppy/osu-web/blob/05488a96b25b5a09f2d97c54c06dd2bae59d1dc8/app/Libraries/Markdown/OsuMarkdown.php#L301
/// </remarks>
public MarkdownPipeline BuildPipeline()
{
var pipeline = new MarkdownPipelineBuilder()
.UseAutoIdentifiers()
.UsePipeTables()
.UseEmphasisExtras(EmphasisExtraOptions.Strikethrough)
.UseYamlFrontMatter();
if (Footnotes)
pipeline = pipeline.UseFootnotes();
if (Autolinks)
pipeline = pipeline.UseAutoLinks();
if (CustomContainers)
pipeline = pipeline.UseCustomContainers();
if (BlockAttributes)
pipeline = pipeline.UseBlockAttributes();
return pipeline.Build();
}
}
}

View File

@ -6,6 +6,7 @@
using System;
using System.Linq;
using Markdig.Extensions.CustomContainers;
using Markdig.Extensions.Footnotes;
using Markdig.Syntax.Inlines;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@ -13,6 +14,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Containers.Markdown.Footnotes;
using osu.Game.Overlays;
using osu.Game.Users;
using osu.Game.Users.Drawables;
@ -36,6 +38,10 @@ namespace osu.Game.Graphics.Containers.Markdown
Text = codeInline.Content
});
protected override void AddFootnoteLink(FootnoteLink footnoteLink) => AddDrawable(new OsuMarkdownFootnoteLink(footnoteLink));
protected override void AddFootnoteBacklink(FootnoteLink footnoteBacklink) => AddDrawable(new OsuMarkdownFootnoteBacklink(footnoteBacklink));
protected override SpriteText CreateEmphasisedSpriteText(bool bold, bool italic)
=> CreateSpriteText().With(t => t.Font = t.Font.With(weight: bold ? FontWeight.Bold : FontWeight.Regular, italics: italic));

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -46,5 +47,8 @@ namespace osu.Game.Graphics.Containers
AddInternal(content);
Add(CreateHoverSounds(sampleSet));
}
protected override void ClearInternal(bool disposeChildren = true) =>
throw new InvalidOperationException($"Clearing {nameof(InternalChildren)} will cause critical failure. Use {nameof(Clear)} instead.");
}
}

View File

@ -22,38 +22,8 @@ namespace osu.Game.Graphics
public static Color4 Gray(byte amt) => new Color4(amt, amt, amt, 255);
/// <summary>
/// Retrieves the colour for a <see cref="DifficultyRating"/>.
/// Retrieves the colour for a given point in the star range.
/// </summary>
/// <remarks>
/// Sourced from the @diff-{rating} variables in https://github.com/ppy/osu-web/blob/71fbab8936d79a7929d13854f5e854b4f383b236/resources/assets/less/variables.less.
/// </remarks>
public Color4 ForDifficultyRating(DifficultyRating difficulty, bool useLighterColour = false)
{
switch (difficulty)
{
case DifficultyRating.Easy:
return Color4Extensions.FromHex("4ebfff");
case DifficultyRating.Normal:
return Color4Extensions.FromHex("66ff91");
case DifficultyRating.Hard:
return Color4Extensions.FromHex("f7e85d");
case DifficultyRating.Insane:
return Color4Extensions.FromHex("ff7e68");
case DifficultyRating.Expert:
return Color4Extensions.FromHex("fe3c71");
case DifficultyRating.ExpertPlus:
return Color4Extensions.FromHex("6662dd");
default:
throw new ArgumentOutOfRangeException(nameof(difficulty));
}
}
public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(new[]
{
(0.1f, Color4Extensions.FromHex("aaaaaa")),

View File

@ -52,8 +52,8 @@ namespace osu.Game.Graphics.UserInterface
public readonly SpriteIcon Chevron;
//don't allow clicking between transitions and don't make the chevron clickable
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Alpha == 1f && Text.ReceivePositionalInputAt(screenSpacePos);
//don't allow clicking between transitions
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Alpha == 1f && base.ReceivePositionalInputAt(screenSpacePos);
public override bool HandleNonPositionalInput => State == Visibility.Visible;
public override bool HandlePositionalInput => State == Visibility.Visible;
@ -95,7 +95,7 @@ namespace osu.Game.Graphics.UserInterface
{
Text.Font = Text.Font.With(size: 18);
Text.Margin = new MarginPadding { Vertical = 8 };
Padding = new MarginPadding { Right = padding + ChevronSize };
Margin = new MarginPadding { Right = padding + ChevronSize };
Add(Chevron = new SpriteIcon
{
Anchor = Anchor.CentreRight,

View File

@ -82,7 +82,7 @@ namespace osu.Game.Graphics.UserInterface
if (Link != null)
{
items.Add(new OsuMenuItem("Open", MenuItemType.Standard, () => host.OpenUrlExternally(Link)));
items.Add(new OsuMenuItem("Open", MenuItemType.Highlighted, () => host.OpenUrlExternally(Link)));
items.Add(new OsuMenuItem("Copy URL", MenuItemType.Standard, copyUrl));
}

View File

@ -45,6 +45,9 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.ShiftPressed)
return false;
switch (e.Key)
{
case Key.Up:

View File

@ -114,8 +114,7 @@ namespace osu.Game.Graphics.UserInterface
get => current;
set
{
if (value == null)
throw new ArgumentNullException(nameof(value));
ArgumentNullException.ThrowIfNull(value);
current.UnbindBindings();
current.BindTo(value);

View File

@ -12,7 +12,7 @@ namespace osu.Game.Graphics.UserInterface
{
public OsuEnumDropdown()
{
Items = (T[])Enum.GetValues(typeof(T));
Items = Enum.GetValues<T>();
}
}
}

View File

@ -23,8 +23,7 @@ namespace osu.Game.IO.FileAbstraction
public void CloseStream(Stream stream)
{
if (stream == null)
throw new ArgumentNullException(nameof(stream));
ArgumentNullException.ThrowIfNull(stream);
stream.Close();
}

View File

@ -14,9 +14,8 @@ namespace osu.Game.IO
{
public static bool CheckAvailability(string testDestinationPath, string testSourcePath)
{
// We can support other operating systems quite easily in the future.
// Let's handle the most common one for now, though.
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
// For simplicity, only support desktop operating systems for now.
if (!RuntimeInfo.IsDesktop)
return false;
const string test_filename = "_hard_link_test";
@ -31,7 +30,7 @@ namespace osu.Game.IO
File.WriteAllText(testSourcePath, string.Empty);
// Test availability by creating an arbitrary hard link between the source and destination paths.
return CreateHardLink(testDestinationPath, testSourcePath, IntPtr.Zero);
return TryCreateHardLink(testDestinationPath, testSourcePath);
}
catch
{
@ -55,21 +54,60 @@ namespace osu.Game.IO
}
}
/// <summary>
/// Attempts to create a hard link from <paramref name="sourcePath"/> to <paramref name="destinationPath"/>,
/// using platform-specific native methods.
/// </summary>
/// <remarks>
/// Hard links are only available on desktop platforms.
/// </remarks>
/// <returns>Whether the hard link was successfully created.</returns>
public static bool TryCreateHardLink(string destinationPath, string sourcePath)
{
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
return CreateHardLink(destinationPath, sourcePath, IntPtr.Zero);
case RuntimeInfo.Platform.Linux:
case RuntimeInfo.Platform.macOS:
return link(sourcePath, destinationPath) == 0;
default:
return false;
}
}
// For future use (to detect if a file is a hard link with other references existing on disk).
public static int GetFileLinkCount(string filePath)
{
int result = 0;
SafeFileHandle handle = CreateFile(filePath, FileAccess.Read, FileShare.Read, IntPtr.Zero, FileMode.Open, FileAttributes.Archive, IntPtr.Zero);
ByHandleFileInformation fileInfo;
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
SafeFileHandle handle = CreateFile(filePath, FileAccess.Read, FileShare.Read, IntPtr.Zero, FileMode.Open, FileAttributes.Archive, IntPtr.Zero);
if (GetFileInformationByHandle(handle, out fileInfo))
result = (int)fileInfo.NumberOfLinks;
CloseHandle(handle);
ByHandleFileInformation fileInfo;
if (GetFileInformationByHandle(handle, out fileInfo))
result = (int)fileInfo.NumberOfLinks;
CloseHandle(handle);
break;
case RuntimeInfo.Platform.Linux:
case RuntimeInfo.Platform.macOS:
if (stat(filePath, out var statbuf) == 0)
result = (int)statbuf.st_nlink;
break;
}
return result;
}
#region Windows native methods
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CreateHardLink(string lpFileName, string lpExistingFileName, IntPtr lpSecurityAttributes);
@ -104,5 +142,49 @@ namespace osu.Game.IO
public readonly uint FileIndexHigh;
public readonly uint FileIndexLow;
}
#endregion
#region Linux native methods
#pragma warning disable IDE1006 // Naming rule violation
[DllImport("libc", SetLastError = true)]
public static extern int link(string oldpath, string newpath);
[DllImport("libc", SetLastError = true)]
private static extern int stat(string pathname, out struct_stat statbuf);
// ReSharper disable once InconsistentNaming
// Struct layout is likely non-portable across unices. Tread with caution.
[StructLayout(LayoutKind.Sequential)]
private struct struct_stat
{
public readonly long st_dev;
public readonly long st_ino;
public readonly long st_nlink;
public readonly int st_mode;
public readonly int st_uid;
public readonly int st_gid;
public readonly long st_rdev;
public readonly long st_size;
public readonly long st_blksize;
public readonly long st_blocks;
public readonly timespec st_atim;
public readonly timespec st_mtim;
public readonly timespec st_ctim;
}
// ReSharper disable once InconsistentNaming
[StructLayout(LayoutKind.Sequential)]
private struct timespec
{
public readonly long tv_sec;
public readonly long tv_nsec;
}
#pragma warning restore IDE1006
#endregion
}
}

View File

@ -64,6 +64,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString RunSetupWizard => new TranslatableString(getKey(@"run_setup_wizard"), @"Run setup wizard");
/// <summary>
/// "Learn more about lazer"
/// </summary>
public static LocalisableString LearnMoreAboutLazer => new TranslatableString(getKey(@"learn_more_about_lazer"), @"Learn more about lazer");
/// <summary>
/// "Check out the feature comparison and FAQ"
/// </summary>
public static LocalisableString LearnMoreAboutLazerTooltip => new TranslatableString(getKey(@"check_out_the_feature_comparison"), @"Check out the feature comparison and FAQ");
/// <summary>
/// "You are running the latest release ({0})"
/// </summary>

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
@ -11,10 +9,11 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using System.Text;
using System.Collections.Generic;
using System.Linq;
namespace osu.Game.Online.API.Requests
{
public class GetScoresRequest : APIRequest<APIScoresCollection>
public class GetScoresRequest : APIRequest<APIScoresCollection>, IEquatable<GetScoresRequest>
{
public const int MAX_SCORES_PER_REQUEST = 50;
@ -23,7 +22,7 @@ namespace osu.Game.Online.API.Requests
private readonly IRulesetInfo ruleset;
private readonly IEnumerable<IMod> mods;
public GetScoresRequest(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable<IMod> mods = null)
public GetScoresRequest(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable<IMod>? mods = null)
{
if (beatmapInfo.OnlineID <= 0)
throw new InvalidOperationException($"Cannot lookup a beatmap's scores without having a populated {nameof(IBeatmapInfo.OnlineID)}.");
@ -51,5 +50,16 @@ namespace osu.Game.Online.API.Requests
return query.ToString();
}
public bool Equals(GetScoresRequest? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return beatmapInfo.Equals(other.beatmapInfo)
&& scope == other.scope
&& ruleset.Equals(other.ruleset)
&& mods.SequenceEqual(other.mods);
}
}
}

View File

@ -9,7 +9,7 @@ namespace osu.Game.Online.API.Requests
{
public class GetUsersRequest : APIRequest<GetUsersResponse>
{
private readonly int[] userIds;
public readonly int[] UserIds;
private const int max_ids_per_request = 50;
@ -18,9 +18,9 @@ namespace osu.Game.Online.API.Requests
if (userIds.Length > max_ids_per_request)
throw new ArgumentException($"{nameof(GetUsersRequest)} calls only support up to {max_ids_per_request} IDs at once");
this.userIds = userIds;
UserIds = userIds;
}
protected override string Target => "users/?ids[]=" + string.Join("&ids[]=", userIds);
protected override string Target => "users/?ids[]=" + string.Join("&ids[]=", UserIds);
}
}

View File

@ -21,7 +21,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty]
private string type
{
set => Type = (RecentActivityType)Enum.Parse(typeof(RecentActivityType), value.ToPascalCase());
set => Type = Enum.Parse<RecentActivityType>(value.ToPascalCase());
}
public RecentActivityType Type;
@ -29,7 +29,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty]
private string scoreRank
{
set => ScoreRank = (ScoreRank)Enum.Parse(typeof(ScoreRank), value);
set => ScoreRank = Enum.Parse<ScoreRank>(value);
}
public ScoreRank ScoreRank;

View File

@ -185,7 +185,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"playstyle")]
private string[] playStyle
{
set => PlayStyles = value?.Select(str => Enum.Parse(typeof(APIPlayStyle), str, true)).Cast<APIPlayStyle>().ToArray();
set => PlayStyles = value?.Select(str => Enum.Parse<APIPlayStyle>(str, true)).ToArray();
}
public APIPlayStyle[] PlayStyles;

View File

@ -98,6 +98,11 @@ namespace osu.Game.Online.Chat
/// </summary>
public Bindable<Message> HighlightedMessage = new Bindable<Message>();
/// <summary>
/// The current text box message while in this <see cref="Channel"/>.
/// </summary>
public Bindable<string> TextBoxMessage = new Bindable<string>(string.Empty);
[JsonConstructor]
public Channel()
{

View File

@ -118,8 +118,7 @@ namespace osu.Game.Online.Chat
/// <param name="name"></param>
public void OpenChannel(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
ArgumentNullException.ThrowIfNull(name);
CurrentChannel.Value = AvailableChannels.FirstOrDefault(c => c.Name == name) ?? throw new ChannelNotFoundException(name);
}
@ -130,8 +129,7 @@ namespace osu.Game.Online.Chat
/// <param name="user">The user the private channel is opened with.</param>
public void OpenPrivateChannel(APIUser user)
{
if (user == null)
throw new ArgumentNullException(nameof(user));
ArgumentNullException.ThrowIfNull(user);
if (user.Id == api.LocalUser.Value.Id)
return;

View File

@ -341,6 +341,8 @@ namespace osu.Game.Online.Chat
OpenWiki,
Custom,
OpenChangelog,
FilterBeatmapSetGenre,
FilterBeatmapSetLanguage,
}
public class Link : IComparable<Link>

View File

@ -111,8 +111,13 @@ namespace osu.Game.Online.Chat
{
drawableChannel?.Expire();
if (e.OldValue != null)
TextBox?.Current.UnbindFrom(e.OldValue.TextBoxMessage);
if (e.NewValue == null) return;
TextBox?.Current.BindTo(e.NewValue.TextBoxMessage);
drawableChannel = CreateDrawableChannel(e.NewValue);
drawableChannel.CreateChatLineAction = CreateMessage;
drawableChannel.Padding = new MarginPadding { Bottom = postingTextBox ? text_box_height : 0 };

View File

@ -9,7 +9,8 @@ namespace osu.Game.Online
{
public ProductionEndpointConfiguration()
{
WebsiteRootUrl = APIEndpointUrl = @"https://osu.ppy.sh";
WebsiteRootUrl = @"https://osu.ppy.sh";
APIEndpointUrl = @"https://lazer.ppy.sh";
APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk";
APIClientID = "5";
SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator";

View File

@ -0,0 +1,42 @@
// 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.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Online.Solo
{
/// <summary>
/// Contains data about the change in a user's profile statistics after completing a score.
/// </summary>
public class SoloStatisticsUpdate
{
/// <summary>
/// The score set by the user that triggered the update.
/// </summary>
public ScoreInfo Score { get; }
/// <summary>
/// The user's profile statistics prior to the score being set.
/// </summary>
public UserStatistics Before { get; }
/// <summary>
/// The user's profile statistics after the score was set.
/// </summary>
public UserStatistics After { get; }
/// <summary>
/// Creates a new <see cref="SoloStatisticsUpdate"/>.
/// </summary>
/// <param name="score">The score set by the user that triggered the update.</param>
/// <param name="before">The user's profile statistics prior to the score being set.</param>
/// <param name="after">The user's profile statistics after the score was set.</param>
public SoloStatisticsUpdate(ScoreInfo score, UserStatistics before, UserStatistics after)
{
Score = score;
Before = before;
After = after;
}
}
}

View File

@ -0,0 +1,162 @@
// 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.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Spectator;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Online.Solo
{
/// <summary>
/// A persistent component that binds to the spectator server and API in order to deliver updates about the logged in user's gameplay statistics.
/// </summary>
public partial class SoloStatisticsWatcher : Component
{
[Resolved]
private SpectatorClient spectatorClient { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
private readonly Dictionary<long, StatisticsUpdateCallback> callbacks = new Dictionary<long, StatisticsUpdateCallback>();
private long? lastProcessedScoreId;
private Dictionary<string, UserStatistics>? latestStatistics;
protected override void LoadComplete()
{
base.LoadComplete();
api.LocalUser.BindValueChanged(user => onUserChanged(user.NewValue), true);
spectatorClient.OnUserScoreProcessed += userScoreProcessed;
}
/// <summary>
/// Registers for a user statistics update after the given <paramref name="score"/> has been processed server-side.
/// </summary>
/// <param name="score">The score to listen for the statistics update for.</param>
/// <param name="onUpdateReady">The callback to be invoked once the statistics update has been prepared.</param>
/// <returns>An <see cref="IDisposable"/> representing the subscription. Disposing it is equivalent to unsubscribing from future notifications.</returns>
public IDisposable RegisterForStatisticsUpdateAfter(ScoreInfo score, Action<SoloStatisticsUpdate> onUpdateReady)
{
Schedule(() =>
{
if (!api.IsLoggedIn)
return;
if (!score.Ruleset.IsLegacyRuleset() || score.OnlineID <= 0)
return;
var callback = new StatisticsUpdateCallback(score, onUpdateReady);
if (lastProcessedScoreId == score.OnlineID)
{
requestStatisticsUpdate(api.LocalUser.Value.Id, callback);
return;
}
callbacks.Add(score.OnlineID, callback);
});
return new InvokeOnDisposal(() => Schedule(() => callbacks.Remove(score.OnlineID)));
}
private void onUserChanged(APIUser? localUser) => Schedule(() =>
{
callbacks.Clear();
lastProcessedScoreId = null;
latestStatistics = null;
if (localUser == null || localUser.OnlineID <= 1)
return;
var userRequest = new GetUsersRequest(new[] { localUser.OnlineID });
userRequest.Success += initialiseUserStatistics;
api.Queue(userRequest);
});
private void initialiseUserStatistics(GetUsersResponse response) => Schedule(() =>
{
var user = response.Users.SingleOrDefault();
// possible if the user is restricted or similar.
if (user == null)
return;
latestStatistics = new Dictionary<string, UserStatistics>();
if (user.RulesetsStatistics != null)
{
foreach (var rulesetStats in user.RulesetsStatistics)
latestStatistics.Add(rulesetStats.Key, rulesetStats.Value);
}
});
private void userScoreProcessed(int userId, long scoreId)
{
if (userId != api.LocalUser.Value?.OnlineID)
return;
lastProcessedScoreId = scoreId;
if (!callbacks.TryGetValue(scoreId, out var callback))
return;
requestStatisticsUpdate(userId, callback);
callbacks.Remove(scoreId);
}
private void requestStatisticsUpdate(int userId, StatisticsUpdateCallback callback)
{
var request = new GetUserRequest(userId, callback.Score.Ruleset);
request.Success += user => Schedule(() => dispatchStatisticsUpdate(callback, user.Statistics));
api.Queue(request);
}
private void dispatchStatisticsUpdate(StatisticsUpdateCallback callback, UserStatistics updatedStatistics)
{
string rulesetName = callback.Score.Ruleset.ShortName;
if (latestStatistics == null)
return;
latestStatistics.TryGetValue(rulesetName, out UserStatistics? latestRulesetStatistics);
latestRulesetStatistics ??= new UserStatistics();
var update = new SoloStatisticsUpdate(callback.Score, latestRulesetStatistics, updatedStatistics);
callback.OnUpdateReady.Invoke(update);
latestStatistics[rulesetName] = updatedStatistics;
}
protected override void Dispose(bool isDisposing)
{
if (spectatorClient.IsNotNull())
spectatorClient.OnUserScoreProcessed -= userScoreProcessed;
base.Dispose(isDisposing);
}
private class StatisticsUpdateCallback
{
public ScoreInfo Score { get; }
public Action<SoloStatisticsUpdate> OnUpdateReady { get; }
public StatisticsUpdateCallback(ScoreInfo score, Action<SoloStatisticsUpdate> onUpdateReady)
{
Score = score;
OnUpdateReady = onUpdateReady;
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More