1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-17 20:47:20 +08:00

Merge branch 'master' into intel-exclusive-warning

This commit is contained in:
Dean Herbert 2022-10-04 19:02:01 +09:00 committed by GitHub
commit 75b9bf2cf9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
112 changed files with 1825 additions and 561 deletions

View File

@ -4,6 +4,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
inspect-code:
name: Code Quality

View File

@ -8,8 +8,12 @@ on:
workflows: ["Continuous Integration"]
types:
- completed
permissions: {}
jobs:
annotate:
permissions:
checks: write # to create checks (dorny/test-reporter)
name: Annotate CI run with test results
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}

View File

@ -5,6 +5,9 @@ on:
tags:
- '*'
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
sentry_release:
runs-on: ubuntu-latest

View File

@ -21,7 +21,6 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
using osuTK;
@ -250,11 +249,9 @@ namespace osu.Game.Rulesets.Catch.Tests
[Test]
public void TestHitLightingColour()
{
var fruitColour = SkinConfiguration.DefaultComboColours[1];
AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true));
AddStep("catch fruit", () => attemptCatch(new Fruit()));
AddAssert("correct hit lighting colour", () =>
catcher.ChildrenOfType<HitExplosion>().First()?.Entry?.ObjectColour == fruitColour);
AddAssert("correct hit lighting colour", () => catcher.ChildrenOfType<HitExplosion>().First()?.Entry?.ObjectColour == this.ChildrenOfType<DrawableCatchHitObject>().First().AccentColour.Value);
}
[Test]

View File

@ -3,7 +3,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
@ -16,22 +15,14 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableFloat SizeMultiplier { get; } = new BindableFloat
public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
{
MinValue = 0.5f,
MaxValue = 1.5f,
Default = 1f,
Value = 1f,
Precision = 0.1f
};
[SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
public override BindableBool ComboBasedSize { get; } = new BindableBool
{
Default = true,
Value = true
};
public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
public override float DefaultFlashlightSize => 350;

View File

@ -6,8 +6,6 @@ using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.UI;
@ -17,15 +15,8 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public override LocalisableString Description => "Where's the catcher?";
[SettingSource(
"Hidden at combo",
"The combo count at which the catcher becomes completely hidden",
SettingControlType = typeof(SettingsSlider<int, HiddenComboSlider>)
)]
public override BindableInt HiddenComboCount { get; } = new BindableInt
public override BindableInt HiddenComboCount { get; } = new BindableInt(10)
{
Default = 10,
Value = 10,
MinValue = 0,
MaxValue = 50,
};

View File

@ -5,7 +5,6 @@ using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Layout;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
using osuTK;
@ -17,22 +16,14 @@ namespace osu.Game.Rulesets.Mania.Mods
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModHidden) };
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableFloat SizeMultiplier { get; } = new BindableFloat
public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
{
MinValue = 0.5f,
MaxValue = 3f,
Default = 1f,
Value = 1f,
Precision = 0.1f
};
[SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
public override BindableBool ComboBasedSize { get; } = new BindableBool
{
Default = false,
Value = false
};
public override BindableBool ComboBasedSize { get; } = new BindableBool();
public override float DefaultFlashlightSize => 50;

View File

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public void UpdateResult() => base.UpdateResult(true);
protected override double MaximumJudgementOffset => base.MaximumJudgementOffset * release_window_lenience;
public override double MaximumJudgementOffset => base.MaximumJudgementOffset * release_window_lenience;
protected override void CheckForResult(bool userTriggered, double timeOffset)
{

View File

@ -0,0 +1,102 @@
// 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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModRandom : OsuModTestScene
{
[TestCase(1)]
[TestCase(7)]
[TestCase(10)]
public void TestDefaultBeatmap(float angleSharpness) => CreateModTest(new ModTestData
{
Mod = new OsuModRandom
{
AngleSharpness = { Value = angleSharpness }
},
Autoplay = true,
PassCondition = () => true
});
[TestCase(1)]
[TestCase(7)]
[TestCase(10)]
public void TestJumpBeatmap(float angleSharpness) => CreateModTest(new ModTestData
{
Mod = new OsuModRandom
{
AngleSharpness = { Value = angleSharpness }
},
Beatmap = jumpBeatmap,
Autoplay = true,
PassCondition = () => true
});
[TestCase(1)]
[TestCase(7)]
[TestCase(10)]
public void TestStreamBeatmap(float angleSharpness) => CreateModTest(new ModTestData
{
Mod = new OsuModRandom
{
AngleSharpness = { Value = angleSharpness }
},
Beatmap = streamBeatmap,
Autoplay = true,
PassCondition = () => true
});
private OsuBeatmap jumpBeatmap =>
createHitCircleBeatmap(new[] { 100, 200, 300, 400 }, 8, 300, 2 * 300);
private OsuBeatmap streamBeatmap =>
createHitCircleBeatmap(new[] { 10, 20, 30, 40, 50, 60, 70, 80 }, 16, 150, 4 * 150);
private OsuBeatmap createHitCircleBeatmap(IEnumerable<int> spacings, int objectsPerSpacing, int interval, int beatLength)
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint
{
Time = 0,
BeatLength = beatLength
});
var beatmap = new OsuBeatmap
{
BeatmapInfo = new BeatmapInfo
{
StackLeniency = 0,
Difficulty = new BeatmapDifficulty
{
ApproachRate = 8.5f
}
},
ControlPointInfo = controlPointInfo
};
foreach (int spacing in spacings)
{
for (int i = 0; i < objectsPerSpacing; i++)
{
beatmap.HitObjects.Add(new HitCircle
{
StartTime = interval * beatmap.HitObjects.Count,
Position = beatmap.HitObjects.Count % 2 == 0 ? Vector2.Zero : new Vector2(spacing, 0),
NewCombo = i == 0
});
}
}
return beatmap;
}
}
}

View File

@ -7,7 +7,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
@ -43,7 +42,6 @@ namespace osu.Game.Rulesets.Osu.Tests
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
private DrawableSpinner drawableSpinner = null!;
private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType<SpriteIcon>().Single();
[SetUpSteps]
public override void SetUpSteps()
@ -77,7 +75,6 @@ namespace osu.Game.Rulesets.Osu.Tests
{
double finalCumulativeTrackerRotation = 0;
double finalTrackerRotation = 0, trackerRotationTolerance = 0;
double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
addSeekStep(spinner_start_time + 5000);
AddStep("retrieve disc rotation", () =>
@ -85,11 +82,6 @@ namespace osu.Game.Rulesets.Osu.Tests
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
trackerRotationTolerance = Math.Abs(finalTrackerRotation * 0.05f);
});
AddStep("retrieve spinner symbol rotation", () =>
{
finalSpinnerSymbolRotation = spinnerSymbol.Rotation;
spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f);
});
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation);
addSeekStep(spinner_start_time + 2500);
@ -98,8 +90,6 @@ namespace osu.Game.Rulesets.Osu.Tests
// due to the exponential damping applied we're allowing a larger margin of error of about 10%
// (5% relative to the final rotation value, but we're half-way through the spin).
() => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation / 2).Within(trackerRotationTolerance));
AddAssert("symbol rotation rewound",
() => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation / 2).Within(spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation rewound",
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
() => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation / 2).Within(100));
@ -107,8 +97,6 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(spinner_start_time + 5000);
AddAssert("is disc rotation almost same",
() => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation).Within(trackerRotationTolerance));
AddAssert("is symbol rotation almost same",
() => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation).Within(spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation almost same",
() => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation).Within(100));
}
@ -122,7 +110,6 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(5000);
AddAssert("disc spin direction correct", () => clockwise ? drawableSpinner.RotationTracker.Rotation > 0 : drawableSpinner.RotationTracker.Rotation < 0);
AddAssert("spinner symbol direction correct", () => clockwise ? spinnerSymbol.Rotation > 0 : spinnerSymbol.Rotation < 0);
}
private Replay flip(Replay scoreReplay) => new Replay

View File

@ -0,0 +1,149 @@
// 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.Audio;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Replays;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneTrianglesSpinnerRotation : TestSceneOsuPlayer
{
private const double spinner_start_time = 100;
private const double spinner_duration = 6000;
[Resolved]
private SkinManager skinManager { get; set; } = null!;
[Resolved]
private AudioManager audioManager { get; set; } = null!;
protected override bool Autoplay => true;
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
private DrawableSpinner drawableSpinner = null!;
private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType<SpriteIcon>().Single();
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("set triangles skin", () => skinManager.CurrentSkinInfo.Value = TrianglesSkin.CreateInfo().ToLiveUnmanaged());
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)Player.DrawableRuleset.Playfield.AllHitObjects.First());
}
[Test]
public void TestSymbolMiddleRewindingRotation()
{
double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
addSeekStep(spinner_start_time + 5000);
AddStep("retrieve spinner symbol rotation", () =>
{
finalSpinnerSymbolRotation = spinnerSymbol.Rotation;
spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f);
});
addSeekStep(spinner_start_time + 2500);
AddAssert("symbol rotation rewound",
() => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation / 2).Within(spinnerSymbolRotationTolerance));
addSeekStep(spinner_start_time + 5000);
AddAssert("is symbol rotation almost same",
() => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation).Within(spinnerSymbolRotationTolerance));
}
[Test]
public void TestSymbolRotationDirection([Values(true, false)] bool clockwise)
{
if (clockwise)
transformReplay(flip);
addSeekStep(5000);
AddAssert("spinner symbol direction correct", () => clockwise ? spinnerSymbol.Rotation > 0 : spinnerSymbol.Rotation < 0);
}
private Replay flip(Replay scoreReplay) => new Replay
{
Frames = scoreReplay
.Frames
.Cast<OsuReplayFrame>()
.Select(replayFrame =>
{
var flippedPosition = new Vector2(OsuPlayfield.BASE_SIZE.X - replayFrame.Position.X, replayFrame.Position.Y);
return new OsuReplayFrame(replayFrame.Time, flippedPosition, replayFrame.Actions.ToArray());
})
.Cast<ReplayFrame>()
.ToList()
};
private void addSeekStep(double time)
{
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(100));
}
private void transformReplay(Func<Replay, Replay> replayTransformation) => AddStep("set replay", () =>
{
var drawableRuleset = this.ChildrenOfType<DrawableOsuRuleset>().Single();
var score = drawableRuleset.ReplayScore;
var transformedScore = new Score
{
ScoreInfo = score.ScoreInfo,
Replay = replayTransformation.Invoke(score.Replay)
};
drawableRuleset.SetReplayScore(transformedScore);
});
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
{
HitObjects = new List<HitObject>
{
new Spinner
{
Position = new Vector2(256, 192),
StartTime = spinner_start_time,
Duration = spinner_duration
},
}
};
private class ScoreExposedPlayer : TestPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
public ScoreExposedPlayer()
: base(false, false)
{
}
}
}
}

View File

@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
if (!(currentObj.BaseObject is Spinner))
{
double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.EndPosition).Length;
double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.StackedEndPosition).Length;
cumulativeStrainTime += lastObj.StrainTime;

View File

@ -4,7 +4,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
namespace osu.Game.Rulesets.Osu.Mods
{
@ -18,13 +17,10 @@ namespace osu.Game.Rulesets.Osu.Mods
public override LocalisableString Description => "Hit them at the right size!";
[SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
public override BindableNumber<float> StartScale { get; } = new BindableFloat
public override BindableNumber<float> StartScale { get; } = new BindableFloat(2)
{
MinValue = 1f,
MaxValue = 25f,
Default = 2f,
Value = 2f,
Precision = 0.1f,
};
}

View File

@ -32,22 +32,14 @@ namespace osu.Game.Rulesets.Osu.Mods
Precision = default_follow_delay,
};
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableFloat SizeMultiplier { get; } = new BindableFloat
public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
{
MinValue = 0.5f,
MaxValue = 2f,
Default = 1f,
Value = 1f,
Precision = 0.1f
};
[SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
public override BindableBool ComboBasedSize { get; } = new BindableBool
{
Default = true,
Value = true
};
public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
public override float DefaultFlashlightSize => 180;

View File

@ -4,7 +4,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
namespace osu.Game.Rulesets.Osu.Mods
{
@ -18,13 +17,10 @@ namespace osu.Game.Rulesets.Osu.Mods
public override LocalisableString Description => "Hit them at the right size!";
[SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
public override BindableNumber<float> StartScale { get; } = new BindableFloat
public override BindableNumber<float> StartScale { get; } = new BindableFloat(0.5f)
{
MinValue = 0f,
MaxValue = 0.99f,
Default = 0.5f,
Value = 0.5f,
Precision = 0.01f,
};
}

View File

@ -7,8 +7,6 @@ using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
@ -22,15 +20,8 @@ namespace osu.Game.Rulesets.Osu.Mods
private PeriodTracker spinnerPeriods = null!;
[SettingSource(
"Hidden at combo",
"The combo count at which the cursor becomes completely hidden",
SettingControlType = typeof(SettingsSlider<int, HiddenComboSlider>)
)]
public override BindableInt HiddenComboCount { get; } = new BindableInt
public override BindableInt HiddenComboCount { get; } = new BindableInt(10)
{
Default = 10,
Value = 10,
MinValue = 0,
MaxValue = 50,
};

View File

@ -4,6 +4,7 @@
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@ -20,6 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1;
[SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
public abstract BindableNumber<float> StartScale { get; }
protected virtual float EndScale => 1;

View File

@ -29,10 +29,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTarget)).ToArray();
[SettingSource("Angle sharpness", "How sharp angles should be", SettingControlType = typeof(SettingsSlider<float>))]
public BindableFloat AngleSharpness { get; } = new BindableFloat
public BindableFloat AngleSharpness { get; } = new BindableFloat(7)
{
Default = 7,
Value = 7,
MinValue = 1,
MaxValue = 10,
Precision = 0.1f

View File

@ -53,11 +53,7 @@ namespace osu.Game.Rulesets.Osu.Mods
}).ToArray();
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
public Bindable<int?> Seed { get; } = new Bindable<int?>
{
Default = null,
Value = null
};
public Bindable<int?> Seed { get; } = new Bindable<int?>();
[SettingSource("Metronome ticks", "Whether a metronome beat should play in the background")]
public Bindable<bool> Metronome { get; } = new BindableBool(true);

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
}
protected override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration;
public override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration;
/// <summary>
/// Apply a judgement result.

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Objects
// This is so on repeats ticks don't appear too late to be visually processed by the player.
offset = 200;
else
offset = TimeFadeIn * 0.66f;
offset = TimePreempt * 0.66f;
TimePreempt = (StartTime - SpanStartTime) / 2 + offset;
}

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 osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonCursor : OsuCursorSprite
{
public ArgonCursor()
{
RelativeSizeAxes = Axes.Both;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
InternalChildren = new[]
{
ExpandTarget = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 6,
BorderColour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.4f,
Colour = Colour4.FromHex("FC618F").Darken(0.6f),
},
new CircularContainer
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 2,
BorderColour = Color4.White.Opacity(0.8f),
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true,
},
},
},
},
},
new Circle
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Scale = new Vector2(0.2f),
Colour = new Color4(255, 255, 255, 255),
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 20,
Colour = new Color4(171, 255, 255, 100),
},
},
};
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonCursorTrail : CursorTrail
{
protected override float IntervalMultiplier => 0.4f;
protected override float FadeExponent => 4;
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
Texture = textures.Get(@"Cursor/cursortrail");
Scale = new Vector2(0.8f / Texture.ScaleAdjust);
Blending = BlendingParameters.Additive;
Alpha = 0.8f;
}
}
}

View File

@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonFollowPoint : CompositeDrawable
{
public ArgonFollowPoint()
{
Blending = BlendingParameters.Additive;
Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41"));
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new SpriteIcon
{
Icon = FontAwesome.Solid.ChevronRight,
Size = new Vector2(8),
Colour = OsuColour.Gray(0.2f),
},
new SpriteIcon
{
Icon = FontAwesome.Solid.ChevronRight,
Size = new Vector2(8),
X = 4,
},
};
}
}
}

View File

@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
const double fade_out_time = 800;
const double flash_in_duration = 150;
const double resize_duration = 300;
const double resize_duration = 400;
const float shrink_size = 0.8f;
@ -165,13 +165,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
// The outer gradient is resize with a slight delay from the border.
// This is to give it a bomb-like effect, with the border "triggering" its animation when getting close.
using (BeginDelayedSequence(flash_in_duration / 12))
{
outerGradient.ResizeTo(outerGradient.Size * shrink_size, resize_duration, Easing.OutElasticHalf);
outerGradient
.FadeColour(Color4.White, 80)
.Then()
.FadeOut(flash_in_duration);
}
// The flash layer starts white to give the wanted brightness, but is almost immediately
// recoloured to the accent colour. This would more correctly be done with two layers (one for the initial flash)
// but works well enough with the colour fade.
flash.FadeTo(1, flash_in_duration, Easing.OutQuint);
flash.FlashColour(Color4.White, flash_in_duration, Easing.OutQuint);
flash.FlashColour(accentColour.Value, fade_out_time, Easing.OutQuint);
this.FadeOut(fade_out_time, Easing.OutQuad);
break;

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
};
accentColour = hitObject.AccentColour.GetBoundCopy();
accentColour.BindValueChanged(accent => BorderColour = accent.NewValue);
accentColour.BindValueChanged(accent => BorderColour = accent.NewValue, true);
}
}
}

View File

@ -0,0 +1,146 @@
// 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.Globalization;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonSpinner : CompositeDrawable
{
private DrawableSpinner drawableSpinner = null!;
private OsuSpriteText bonusCounter = null!;
private Container spmContainer = null!;
private OsuSpriteText spmCounter = null!;
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableHitObject)
{
RelativeSizeAxes = Axes.Both;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
drawableSpinner = (DrawableSpinner)drawableHitObject;
InternalChildren = new Drawable[]
{
bonusCounter = new OsuSpriteText
{
Alpha = 0,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Default.With(size: 24),
Y = -120,
},
new ArgonSpinnerDisc
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
bonusCounter = new OsuSpriteText
{
Alpha = 0,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Default.With(size: 28, weight: FontWeight.Bold),
Y = -100,
},
spmContainer = new Container
{
Alpha = 0f,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = 60,
Children = new[]
{
spmCounter = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = @"0",
Font = OsuFont.Default.With(size: 28, weight: FontWeight.SemiBold)
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = @"SPINS PER MINUTE",
Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold),
Y = 30
}
}
}
};
}
private IBindable<double> gainedBonus = null!;
private IBindable<double> spinsPerMinute = null!;
protected override void LoadComplete()
{
base.LoadComplete();
gainedBonus = drawableSpinner.GainedBonus.GetBoundCopy();
gainedBonus.BindValueChanged(bonus =>
{
bonusCounter.Text = bonus.NewValue.ToString(NumberFormatInfo.InvariantInfo);
bonusCounter.FadeOutFromOne(1500);
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
});
spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy();
spinsPerMinute.BindValueChanged(spm =>
{
spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
}, true);
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
}
protected override void Update()
{
base.Update();
if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null)
fadeCounterOnTimeStart();
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
if (!(drawableHitObject is DrawableSpinner))
return;
fadeCounterOnTimeStart();
}
private void fadeCounterOnTimeStart()
{
if (drawableSpinner.Result?.TimeStarted is double startTime)
{
using (BeginAbsoluteSequence(startTime))
spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableSpinner.IsNotNull())
drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}

View File

@ -0,0 +1,247 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonSpinnerDisc : CompositeDrawable
{
private const float initial_scale = 1f;
private const float idle_alpha = 0.2f;
private const float tracking_alpha = 0.4f;
private const float idle_centre_size = 80f;
private const float tracking_centre_size = 40f;
private DrawableSpinner drawableSpinner = null!;
private readonly BindableBool complete = new BindableBool();
private int wholeRotationCount;
private bool checkNewRotationCount
{
get
{
int rotations = (int)(drawableSpinner.Result.RateAdjustedRotation / 360);
if (wholeRotationCount == rotations) return false;
wholeRotationCount = rotations;
return true;
}
}
private Container disc = null!;
private Container centre = null!;
private CircularContainer fill = null!;
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableHitObject)
{
drawableSpinner = (DrawableSpinner)drawableHitObject;
// we are slightly bigger than our parent, to clip the top and bottom of the circle
// this should probably be revisited when scaled spinners are a thing.
Scale = new Vector2(initial_scale);
InternalChildren = new Drawable[]
{
disc = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
fill = new CircularContainer
{
Name = @"Fill",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = Colour4.FromHex("FC618F").Opacity(1f),
Radius = 40,
},
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0f,
AlwaysPresent = true,
}
},
new CircularContainer
{
Name = @"Ring",
Masking = true,
BorderColour = Color4.White,
BorderThickness = 5,
RelativeSizeAxes = Axes.Both,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true,
}
},
new ArgonSpinnerTicks(),
}
},
centre = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(idle_centre_size),
Children = new[]
{
new RingPiece(10)
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.8f),
},
new RingPiece(3)
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(1f),
}
},
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
}
protected override void Update()
{
base.Update();
complete.Value = Time.Current >= drawableSpinner.Result.TimeCompleted;
if (complete.Value)
{
if (checkNewRotationCount)
{
fill.FinishTransforms(false, nameof(Alpha));
fill
.FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo)
.Then()
.FadeTo(tracking_alpha, 250, Easing.OutQuint);
}
}
else
{
fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Math.Abs(Clock.ElapsedFrameTime));
}
if (centre.Width == idle_centre_size && drawableSpinner.Result?.TimeStarted != null)
updateCentrePieceSize();
const float initial_fill_scale = 0.1f;
float targetScale = initial_fill_scale + (0.98f - initial_fill_scale) * drawableSpinner.Progress;
fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
disc.Rotation = drawableSpinner.RotationTracker.Rotation;
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
if (!(drawableHitObject is DrawableSpinner))
return;
Spinner spinner = drawableSpinner.HitObject;
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
this.ScaleTo(initial_scale);
this.RotateTo(0);
using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
// constant ambient rotation to give the spinner "spinning" character.
this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
}
using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset))
{
switch (state)
{
case ArmedState.Hit:
this.ScaleTo(initial_scale * 1.2f, 320, Easing.Out);
this.RotateTo(Rotation + 180, 320);
break;
case ArmedState.Miss:
this.ScaleTo(initial_scale * 0.8f, 320, Easing.In);
break;
}
}
}
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
centre.ScaleTo(0);
disc.ScaleTo(0);
using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
disc.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
centre.ScaleTo(0.8f, spinner.TimePreempt / 2, Easing.OutQuint);
disc.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
}
}
}
if (drawableSpinner.Result?.TimeStarted != null)
updateCentrePieceSize();
}
private void updateCentrePieceSize()
{
Debug.Assert(drawableSpinner.Result?.TimeStarted != null);
Spinner spinner = drawableSpinner.HitObject;
using (BeginAbsoluteSequence(drawableSpinner.Result.TimeStarted.Value))
centre.ResizeTo(new Vector2(tracking_centre_size), spinner.TimePreempt / 2, Easing.OutQuint);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableSpinner.IsNotNull())
drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}

View File

@ -0,0 +1,61 @@
// 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;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonSpinnerTicks : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load()
{
Origin = Anchor.Centre;
Anchor = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
const float count = 25;
for (float i = 0; i < count; i++)
{
AddInternal(new CircularContainer
{
RelativePositionAxes = Axes.Both,
Masking = true,
CornerRadius = 5,
BorderColour = Color4.White,
BorderThickness = 2f,
Size = new Vector2(30, 5),
Origin = Anchor.Centre,
Position = new Vector2(
0.5f + MathF.Sin(i / count * 2 * MathF.PI) / 2 * 0.75f,
0.5f + MathF.Cos(i / count * 2 * MathF.PI) / 2 * 0.75f
),
Rotation = -i / count * 360 - 120,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = Colour4.White.Opacity(0.2f),
Radius = 30,
},
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true,
}
}
});
}
}
}
}

View File

@ -42,8 +42,20 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
case OsuSkinComponents.SliderScorePoint:
return new ArgonSliderScorePoint();
case OsuSkinComponents.SpinnerBody:
return new ArgonSpinner();
case OsuSkinComponents.ReverseArrow:
return new ArgonReverseArrow();
case OsuSkinComponents.FollowPoint:
return new ArgonFollowPoint();
case OsuSkinComponents.Cursor:
return new ArgonCursor();
case OsuSkinComponents.CursorTrail:
return new ArgonCursorTrail();
}
break;

View File

@ -3,6 +3,7 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
@ -129,5 +130,32 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
AssertResult<Hit>(0, HitResult.Miss);
AssertResult<StrongNestedHitObject>(0, HitResult.IgnoreMiss);
}
[Test]
public void TestHighVelocityHit()
{
const double hit_time = 1000;
var beatmap = CreateBeatmap(new Hit
{
Type = HitType.Centre,
StartTime = hit_time,
});
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 6 });
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 10 });
var hitWindows = new HitWindows();
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
PerformTest(new List<ReplayFrame>
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(hit_time - hitWindows.WindowFor(HitResult.Great), TaikoAction.LeftCentre),
}, beatmap);
AssertJudgementCount(1);
AssertResult<Hit>(0, HitResult.Ok);
}
}
}

View File

@ -4,7 +4,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Layout;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
@ -17,22 +16,14 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableFloat SizeMultiplier { get; } = new BindableFloat
public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
{
MinValue = 0.5f,
MaxValue = 1.5f,
Default = 1f,
Value = 1f,
Precision = 0.1f
};
[SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
public override BindableBool ComboBasedSize { get; } = new BindableBool
{
Default = true,
Value = true
};
public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
public override float DefaultFlashlightSize => 250;

View File

@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
Filled = HitObject.FirstTick
});
protected override double MaximumJudgementOffset => HitObject.HitWindow;
public override double MaximumJudgementOffset => HitObject.HitWindow;
protected override void CheckForResult(bool userTriggered, double timeOffset)
{

View File

@ -204,31 +204,23 @@ namespace osu.Game.Tests.Online
public override double ScoreMultiplier => 1;
[SettingSource("Initial rate", "The starting speed of the track")]
public override BindableNumber<double> InitialRate { get; } = new BindableDouble
public override BindableNumber<double> InitialRate { get; } = new BindableDouble(1.5)
{
MinValue = 1,
MaxValue = 2,
Default = 1.5,
Value = 1.5,
Precision = 0.01,
};
[SettingSource("Final rate", "The speed increase to ramp towards")]
public override BindableNumber<double> FinalRate { get; } = new BindableDouble
public override BindableNumber<double> FinalRate { get; } = new BindableDouble(0.5)
{
MinValue = 0,
MaxValue = 1,
Default = 0.5,
Value = 0.5,
Precision = 0.01,
};
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public override BindableBool AdjustPitch { get; } = new BindableBool
{
Default = true,
Value = true
};
public override BindableBool AdjustPitch { get; } = new BindableBool(true);
}
private class TestModDifficultyAdjust : ModDifficultyAdjust

View File

@ -124,31 +124,23 @@ namespace osu.Game.Tests.Online
public override double ScoreMultiplier => 1;
[SettingSource("Initial rate", "The starting speed of the track")]
public override BindableNumber<double> InitialRate { get; } = new BindableDouble
public override BindableNumber<double> InitialRate { get; } = new BindableDouble(1.5)
{
MinValue = 1,
MaxValue = 2,
Default = 1.5,
Value = 1.5,
Precision = 0.01,
};
[SettingSource("Final rate", "The speed increase to ramp towards")]
public override BindableNumber<double> FinalRate { get; } = new BindableDouble
public override BindableNumber<double> FinalRate { get; } = new BindableDouble(0.5)
{
MinValue = 0,
MaxValue = 1,
Default = 0.5,
Value = 0.5,
Precision = 0.01,
};
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public override BindableBool AdjustPitch { get; } = new BindableBool
{
Default = true,
Value = true
};
public override BindableBool AdjustPitch { get; } = new BindableBool(true);
}
private class TestModEnum : Mod

View File

@ -8,7 +8,6 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
@ -78,7 +77,7 @@ namespace osu.Game.Tests.Online
}
};
beatmaps.AllowImport = new TaskCompletionSource<bool>();
beatmaps.AllowImport.Reset();
testBeatmapFile = TestResources.GetQuickTestBeatmapForImport();
@ -132,7 +131,7 @@ namespace osu.Game.Tests.Online
AddStep("finish download", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet))!.TriggerSuccess(testBeatmapFile));
addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing);
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
AddStep("allow importing", () => beatmaps.AllowImport.Set());
AddUntilStep("wait for import", () => beatmaps.CurrentImport != null);
AddUntilStep("ensure beatmap available", () => beatmaps.IsAvailableLocally(testBeatmapSet));
addAvailabilityCheckStep("state is locally available", BeatmapAvailability.LocallyAvailable);
@ -141,7 +140,7 @@ namespace osu.Game.Tests.Online
[Test]
public void TestTrackerRespectsSoftDeleting()
{
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
AddStep("allow importing", () => beatmaps.AllowImport.Set());
AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).WaitSafely());
addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable);
@ -155,7 +154,7 @@ namespace osu.Game.Tests.Online
[Test]
public void TestTrackerRespectsChecksum()
{
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
AddStep("allow importing", () => beatmaps.AllowImport.Set());
AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).WaitSafely());
addAvailabilityCheckStep("initially locally available", BeatmapAvailability.LocallyAvailable);
@ -202,7 +201,7 @@ namespace osu.Game.Tests.Online
private class TestBeatmapManager : BeatmapManager
{
public TaskCompletionSource<bool> AllowImport = new TaskCompletionSource<bool>();
public readonly ManualResetEventSlim AllowImport = new ManualResetEventSlim();
public Live<BeatmapSetInfo> CurrentImport { get; private set; }
@ -229,7 +228,9 @@ namespace osu.Game.Tests.Online
public override Live<BeatmapSetInfo> ImportModel(BeatmapSetInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default)
{
testBeatmapManager.AllowImport.Task.WaitSafely();
if (!testBeatmapManager.AllowImport.Wait(TimeSpan.FromSeconds(10), cancellationToken))
throw new TimeoutException("Timeout waiting for import to be allowed.");
return (testBeatmapManager.CurrentImport = base.ImportModel(item, archive, batchImport, cancellationToken));
}
}

View File

@ -58,6 +58,16 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for some scores not masked away",
() => leaderboard.ChildrenOfType<GameplayLeaderboardScore>().Any(s => leaderboard.ScreenSpaceDrawQuad.Contains(s.ScreenSpaceDrawQuad.Centre)));
AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad));
AddStep("change score to middle", () => playerScore.Value = 1000000);
AddWaitStep("wait for movement", 5);
AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad));
AddStep("change score to first", () => playerScore.Value = 5000000);
AddWaitStep("wait for movement", 5);
AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad));
}
[Test]

View File

@ -0,0 +1,100 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSoloGameplayLeaderboard : OsuTestScene
{
[Cached]
private readonly ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
private readonly BindableList<ScoreInfo> scores = new BindableList<ScoreInfo>();
private readonly Bindable<bool> configVisibility = new Bindable<bool>();
private SoloGameplayLeaderboard leaderboard = null!;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility);
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("clear scores", () => scores.Clear());
AddStep("create component", () =>
{
var trackingUser = new APIUser
{
Username = "local user",
Id = 2,
};
Child = leaderboard = new SoloGameplayLeaderboard(trackingUser)
{
Scores = { BindTarget = scores },
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AlwaysVisible = { Value = false },
Expanded = { Value = true },
};
});
AddStep("add scores", () => scores.AddRange(createSampleScores()));
}
[Test]
public void TestLocalUser()
{
AddSliderStep("score", 0, 1000000, 500000, v => scoreProcessor.TotalScore.Value = v);
AddSliderStep("accuracy", 0f, 1f, 0.5f, v => scoreProcessor.Accuracy.Value = v);
AddSliderStep("combo", 0, 1000, 0, v => scoreProcessor.HighestCombo.Value = v);
}
[Test]
public void TestVisibility()
{
AddStep("set config visible true", () => configVisibility.Value = true);
AddUntilStep("leaderboard visible", () => leaderboard.Alpha == 1);
AddStep("set config visible false", () => configVisibility.Value = false);
AddUntilStep("leaderboard not visible", () => leaderboard.Alpha == 0);
AddStep("set always visible", () => leaderboard.AlwaysVisible.Value = true);
AddUntilStep("leaderboard visible", () => leaderboard.Alpha == 1);
AddStep("set config visible true", () => configVisibility.Value = true);
AddAssert("leaderboard still visible", () => leaderboard.Alpha == 1);
}
private static List<ScoreInfo> createSampleScores()
{
return new[]
{
new ScoreInfo { User = new APIUser { Username = @"peppy" }, TotalScore = RNG.Next(500000, 1000000) },
new ScoreInfo { User = new APIUser { Username = @"smoogipoo" }, TotalScore = RNG.Next(500000, 1000000) },
new ScoreInfo { User = new APIUser { Username = @"spaceman_atlas" }, TotalScore = RNG.Next(500000, 1000000) },
new ScoreInfo { User = new APIUser { Username = @"frenzibyte" }, TotalScore = RNG.Next(500000, 1000000) },
new ScoreInfo { User = new APIUser { Username = @"Susko3" }, TotalScore = RNG.Next(500000, 1000000) },
}.Concat(Enumerable.Range(0, 50).Select(i => new ScoreInfo { User = new APIUser { Username = $"User {i + 1}" }, TotalScore = 1000000 - i * 10000 })).ToList();
}
}
}

View File

@ -32,6 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected new OutroPlayer Player => (OutroPlayer)base.Player;
private double currentBeatmapDuration;
private double currentStoryboardDuration;
private bool showResults = true;
@ -45,7 +46,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("enable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("set dim level to 0", () => LocalConfig.SetValue<double>(OsuSetting.DimLevel, 0));
AddStep("reset fail conditions", () => currentFailConditions = (_, _) => false);
AddStep("set storyboard duration to 2s", () => currentStoryboardDuration = 2000);
AddStep("set beatmap duration to 0s", () => currentBeatmapDuration = 0);
AddStep("set storyboard duration to 8s", () => currentStoryboardDuration = 8000);
AddStep("set ShowResults = true", () => showResults = true);
}
@ -151,6 +153,24 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("player exited", () => Stack.CurrentScreen == null);
}
[Test]
public void TestPerformExitAfterOutro()
{
CreateTest(() =>
{
AddStep("set beatmap duration to 4s", () => currentBeatmapDuration = 4000);
AddStep("set storyboard duration to 1s", () => currentStoryboardDuration = 1000);
});
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration);
AddStep("exit via pause", () => Player.ExitViaPause());
AddAssert("player paused", () => !Player.IsResuming);
AddStep("resume player", () => Player.Resume());
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
}
protected override bool AllowFail => true;
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
@ -160,7 +180,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap();
beatmap.HitObjects.Add(new HitCircle());
beatmap.HitObjects.Add(new HitCircle { StartTime = currentBeatmapDuration });
return beatmap;
}
@ -189,7 +209,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private event Func<HealthProcessor, JudgementResult, bool> failConditions;
public OutroPlayer(Func<HealthProcessor, JudgementResult, bool> failConditions, bool showResults = true)
: base(false, showResults)
: base(showResults: showResults)
{
this.failConditions = failConditions;
}

View File

@ -120,6 +120,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
=> AddStep($"set user {userId} time {time}", () => clocks[userId].CurrentTime = time);
private void assertCombo(int userId, int expectedCombo)
=> AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo);
=> AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.OnlineID == userId).Combo.Value == expectedCombo);
}
}

View File

@ -522,7 +522,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType<PlayerArea>().Single(p => p.UserId == userId);
private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.Id == userId);
private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.OnlineID == userId);
private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray();
}

View File

@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestEditDefaultSkin()
{
AddAssert("is default skin", () => skinManager.CurrentSkinInfo.Value.ID == SkinInfo.TRIANGLES_SKIN);
AddAssert("is default skin", () => skinManager.CurrentSkinInfo.Value.ID == SkinInfo.ARGON_SKIN);
AddStep("open settings", () => { Game.Settings.Show(); });
@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("open skin editor", () => skinEditor.Show());
// Until step required as the skin editor may take time to load (and an extra scheduled frame for the mutable part).
AddUntilStep("is modified default skin", () => skinManager.CurrentSkinInfo.Value.ID != SkinInfo.TRIANGLES_SKIN);
AddUntilStep("is modified default skin", () => skinManager.CurrentSkinInfo.Value.ID != SkinInfo.ARGON_SKIN);
AddAssert("is not protected", () => skinManager.CurrentSkinInfo.Value.PerformRead(s => !s.Protected));
AddUntilStep("export button enabled", () => Game.Settings.ChildrenOfType<SkinSection.ExportSkinButton>().SingleOrDefault()?.Enabled.Value == true);

View File

@ -29,11 +29,7 @@ namespace osu.Game.Tests.Visual.Settings
{
Child = textBox = new SettingsTextBox
{
Current = new Bindable<string>
{
Default = "test",
Value = "test"
}
Current = new Bindable<string>("test")
};
});
AddUntilStep("wait for loaded", () => textBox.IsLoaded);
@ -59,11 +55,7 @@ namespace osu.Game.Tests.Visual.Settings
{
Child = textBox = new SettingsTextBox
{
Current = new Bindable<string>
{
Default = "test",
Value = "test"
}
Current = new Bindable<string>("test")
};
});
AddUntilStep("wait for loaded", () => textBox.IsLoaded);

View File

@ -67,11 +67,7 @@ namespace osu.Game.Tests.Visual.Settings
};
[SettingSource("Sample number textbox", "Textbox number entry", SettingControlType = typeof(SettingsNumberBox))]
public Bindable<int?> IntTextBoxBindable { get; } = new Bindable<int?>
{
Default = null,
Value = null
};
public Bindable<int?> IntTextBoxBindable { get; } = new Bindable<int?>();
}
private enum TestEnum

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.Collections.Generic;
using System.Linq;
@ -36,10 +34,9 @@ namespace osu.Game.Tests.Visual.SongSelect
[Cached(typeof(IDialogOverlay))]
private readonly DialogOverlay dialogOverlay;
private ScoreManager scoreManager;
private RulesetStore rulesetStore;
private BeatmapManager beatmapManager;
private ScoreManager scoreManager = null!;
private RulesetStore rulesetStore = null!;
private BeatmapManager beatmapManager = null!;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
@ -74,7 +71,7 @@ namespace osu.Game.Tests.Visual.SongSelect
[Test]
public void TestLocalScoresDisplay()
{
BeatmapInfo beatmapInfo = null;
BeatmapInfo beatmapInfo = null!;
AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local);
@ -387,7 +384,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private class FailableLeaderboard : BeatmapLeaderboard
{
public new void SetErrorState(LeaderboardState state) => base.SetErrorState(state);
public new void SetScores(IEnumerable<ScoreInfo> scores, ScoreInfo userScore = default) => base.SetScores(scores, userScore);
public new void SetScores(IEnumerable<ScoreInfo>? scores, ScoreInfo? userScore = null) => base.SetScores(scores, userScore);
}
}
}

View File

@ -163,7 +163,7 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.PressButton(MouseButton.Left);
});
AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
AddUntilStep("wait for fetch", () => leaderboard.Scores.Any());
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != scoreBeingDeleted.OnlineID));
// "Clean up"
@ -174,7 +174,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestDeleteViaDatabase()
{
AddStep("delete top score", () => scoreManager.Delete(importedScores[0]));
AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
AddUntilStep("wait for fetch", () => leaderboard.Scores.Any());
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != importedScores[0].OnlineID));
}
}

View File

@ -107,9 +107,9 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("start drag", () =>
{
InputManager.MoveMouseTo(notification.ChildrenOfType<Notification>().Single());
InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType<Notification>().Single());
InputManager.PressButton(MouseButton.Left);
InputManager.MoveMouseTo(notification.ChildrenOfType<Notification>().Single().ScreenSpaceDrawQuad.Centre + new Vector2(-500, 0));
InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType<Notification>().Single().ScreenSpaceDrawQuad.Centre + new Vector2(-500, 0));
});
AddStep("fling away", () =>
@ -123,6 +123,45 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("unread count zero", () => notificationOverlay.UnreadCount.Value == 0);
}
[Test]
public void TestProgressNotificationCantBeFlung()
{
bool activated = false;
ProgressNotification notification = null!;
AddStep("post", () =>
{
activated = false;
notificationOverlay.Post(notification = new ProgressNotification
{
Text = @"Uploading to BSS...",
CompletionText = "Uploaded to BSS!",
Activated = () => activated = true,
});
progressingNotifications.Add(notification);
});
AddStep("start drag", () =>
{
InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType<Notification>().Single());
InputManager.PressButton(MouseButton.Left);
InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType<Notification>().Single().ScreenSpaceDrawQuad.Centre + new Vector2(-500, 0));
});
AddStep("attempt fling", () =>
{
InputManager.ReleaseButton(MouseButton.Left);
});
AddUntilStep("was not closed", () => !notification.WasClosed);
AddUntilStep("was not cancelled", () => notification.State == ProgressNotificationState.Active);
AddAssert("was not activated", () => !activated);
AddStep("reset mouse position", () => InputManager.MoveMouseTo(Vector2.Zero));
AddUntilStep("was completed", () => notification.State == ProgressNotificationState.Completed);
}
[Test]
public void TestDismissWithoutActivationCloseButton()
{
@ -465,7 +504,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
base.Update();
progressingNotifications.RemoveAll(n => n.State == ProgressNotificationState.Completed);
progressingNotifications.RemoveAll(n => n.State == ProgressNotificationState.Completed && n.WasClosed);
if (progressingNotifications.Count(n => n.State == ProgressNotificationState.Active) < 3)
{

View File

@ -24,7 +24,6 @@ namespace osu.Game.Beatmaps.ControlPoints
public readonly BindableDouble SliderVelocityBindable = new BindableDouble(1)
{
Precision = 0.01,
Default = 1,
MinValue = 0.1,
MaxValue = 10
};

View File

@ -28,7 +28,6 @@ namespace osu.Game.Beatmaps.ControlPoints
public readonly BindableDouble ScrollSpeedBindable = new BindableDouble(1)
{
Precision = 0.01,
Default = 1,
MinValue = 0.01,
MaxValue = 10
};

View File

@ -45,7 +45,6 @@ namespace osu.Game.Beatmaps.ControlPoints
{
MinValue = 0,
MaxValue = 100,
Default = 100
};
/// <summary>

View File

@ -49,7 +49,6 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary>
public readonly BindableDouble BeatLengthBindable = new BindableDouble(DEFAULT_BEAT_LENGTH)
{
Default = DEFAULT_BEAT_LENGTH,
MinValue = 6,
MaxValue = 60000
};

View File

@ -39,7 +39,7 @@ namespace osu.Game.Configuration
{
// UI/selection defaults
SetDefault(OsuSetting.Ruleset, string.Empty);
SetDefault(OsuSetting.Skin, SkinInfo.TRIANGLES_SKIN.ToString());
SetDefault(OsuSetting.Skin, SkinInfo.ARGON_SKIN.ToString());
SetDefault(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details);
SetDefault(OsuSetting.BeatmapDetailModsFilter, false);
@ -131,6 +131,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true);
SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true);
SetDefault(OsuSetting.KeyOverlay, false);
SetDefault(OsuSetting.GameplayLeaderboard, true);
SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true);
SetDefault(OsuSetting.FloatingComments, false);
@ -294,6 +295,7 @@ namespace osu.Game.Configuration
LightenDuringBreaks,
ShowStoryboard,
KeyOverlay,
GameplayLeaderboard,
PositionalHitsounds,
PositionalHitsoundsLevel,
AlwaysPlayFirstComboBreak,

View File

@ -79,6 +79,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString AlwaysShowKeyOverlay => new TranslatableString(getKey(@"key_overlay"), @"Always show key overlay");
/// <summary>
/// "Always show gameplay leaderboard"
/// </summary>
public static LocalisableString AlwaysShowGameplayLeaderboard => new TranslatableString(getKey(@"gameplay_leaderboard"), @"Always show gameplay leaderboard");
/// <summary>
/// "Always play first combo break sound"
/// </summary>

View File

@ -9,27 +9,25 @@ namespace osu.Game.Online.API.Requests
{
public class GetBeatmapRequest : APIRequest<APIBeatmap>
{
private readonly IBeatmapInfo beatmapInfo;
private readonly string filename;
public readonly IBeatmapInfo BeatmapInfo;
public readonly string Filename;
public GetBeatmapRequest(IBeatmapInfo beatmapInfo)
{
this.beatmapInfo = beatmapInfo;
filename = (beatmapInfo as BeatmapInfo)?.Path ?? string.Empty;
BeatmapInfo = beatmapInfo;
Filename = (beatmapInfo as BeatmapInfo)?.Path ?? string.Empty;
}
protected override WebRequest CreateWebRequest()
{
var request = base.CreateWebRequest();
if (beatmapInfo.OnlineID > 0)
request.AddParameter(@"id", beatmapInfo.OnlineID.ToString());
if (!string.IsNullOrEmpty(beatmapInfo.MD5Hash))
request.AddParameter(@"checksum", beatmapInfo.MD5Hash);
if (!string.IsNullOrEmpty(filename))
request.AddParameter(@"filename", filename);
if (BeatmapInfo.OnlineID > 0)
request.AddParameter(@"id", BeatmapInfo.OnlineID.ToString());
if (!string.IsNullOrEmpty(BeatmapInfo.MD5Hash))
request.AddParameter(@"checksum", BeatmapInfo.MD5Hash);
if (!string.IsNullOrEmpty(Filename))
request.AddParameter(@"filename", Filename);
return request;
}

View File

@ -228,7 +228,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"rank_history")]
private APIRankHistory rankHistory
{
set => statistics.RankHistory = value;
set => Statistics.RankHistory = value;
}
[JsonProperty("badges")]

View File

@ -1,14 +1,11 @@
// 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.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Development;
@ -39,7 +36,9 @@ namespace osu.Game.Online.Leaderboards
/// <summary>
/// The currently displayed scores.
/// </summary>
public IEnumerable<TScoreInfo> Scores => scores;
public IBindableList<TScoreInfo> Scores => scores;
private readonly BindableList<TScoreInfo> scores = new BindableList<TScoreInfo>();
/// <summary>
/// Whether the current scope should refetch in response to changes in API connectivity state.
@ -52,25 +51,23 @@ namespace osu.Game.Online.Leaderboards
private readonly Container placeholderContainer;
private readonly UserTopScoreContainer<TScoreInfo> userScoreContainer;
private FillFlowContainer<LeaderboardScore> scoreFlowContainer;
private FillFlowContainer<LeaderboardScore>? scoreFlowContainer;
private readonly LoadingSpinner loading;
private CancellationTokenSource currentFetchCancellationSource;
private CancellationTokenSource currentScoresAsyncLoadCancellationSource;
private CancellationTokenSource? currentFetchCancellationSource;
private CancellationTokenSource? currentScoresAsyncLoadCancellationSource;
private APIRequest fetchScoresRequest;
private APIRequest? fetchScoresRequest;
private LeaderboardState state;
[Resolved(CanBeNull = true)]
private IAPIProvider api { get; set; }
private IAPIProvider? api { get; set; }
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
private ICollection<TScoreInfo> scores;
private TScope scope;
private TScope scope = default!;
public TScope Scope
{
@ -169,7 +166,7 @@ namespace osu.Game.Online.Leaderboards
throw new InvalidOperationException($"State {state} cannot be set by a leaderboard implementation.");
}
Debug.Assert(scores?.Any() != true);
Debug.Assert(!scores.Any());
setState(state);
}
@ -179,17 +176,33 @@ namespace osu.Game.Online.Leaderboards
/// </summary>
/// <param name="scores">The scores to display.</param>
/// <param name="userScore">The user top score, if any.</param>
protected void SetScores(IEnumerable<TScoreInfo> scores, TScoreInfo userScore = default)
protected void SetScores(IEnumerable<TScoreInfo>? scores, TScoreInfo? userScore = default)
{
this.scores = scores?.ToList();
userScoreContainer.Score.Value = userScore;
this.scores.Clear();
if (scores != null)
this.scores.AddRange(scores);
if (userScore == null)
userScoreContainer.Hide();
else
userScoreContainer.Show();
// Non-delayed schedule may potentially run inline (due to IsMainThread check passing) after leaderboard is disposed.
// This is guarded against in BeatmapLeaderboard via web request cancellation, but let's be extra safe.
if (!IsDisposed)
{
// Schedule needs to be non-delayed here for the weird logic in refetchScores to work.
// If it is removed, the placeholder will be incorrectly updated to "no scores" rather than "retrieving".
// This whole flow should be refactored in the future.
Scheduler.Add(applyNewScores, false);
}
Scheduler.Add(updateScoresDrawables, false);
void applyNewScores()
{
userScoreContainer.Score.Value = userScore;
if (userScore == null)
userScoreContainer.Hide();
else
userScoreContainer.Show();
updateScoresDrawables();
}
}
/// <summary>
@ -197,8 +210,7 @@ namespace osu.Game.Online.Leaderboards
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns>An <see cref="APIRequest"/> responsible for the fetch operation. This will be queued and performed automatically.</returns>
[CanBeNull]
protected abstract APIRequest FetchScores(CancellationToken cancellationToken);
protected abstract APIRequest? FetchScores(CancellationToken cancellationToken);
protected abstract LeaderboardScore CreateDrawableScore(TScoreInfo model, int index);
@ -209,8 +221,8 @@ namespace osu.Game.Online.Leaderboards
Debug.Assert(ThreadSafety.IsUpdateThread);
cancelPendingWork();
SetScores(null);
SetScores(null);
setState(LeaderboardState.Retrieving);
currentFetchCancellationSource = new CancellationTokenSource();
@ -247,7 +259,7 @@ namespace osu.Game.Online.Leaderboards
.Expire();
scoreFlowContainer = null;
if (scores?.Any() != true)
if (!scores.Any())
{
setState(LeaderboardState.NoScores);
return;
@ -282,7 +294,7 @@ namespace osu.Game.Online.Leaderboards
#region Placeholder handling
private Placeholder placeholder;
private Placeholder? placeholder;
private void setState(LeaderboardState state)
{
@ -309,7 +321,7 @@ namespace osu.Game.Online.Leaderboards
placeholder.FadeInFromZero(fade_duration, Easing.OutQuint);
}
private Placeholder getPlaceholderFor(LeaderboardState state)
private Placeholder? getPlaceholderFor(LeaderboardState state)
{
switch (state)
{

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.Threading;
using osu.Framework.Bindables;
@ -18,13 +16,15 @@ namespace osu.Game.Online.Leaderboards
{
private const int duration = 500;
public Bindable<TScoreInfo> Score = new Bindable<TScoreInfo>();
public Bindable<TScoreInfo?> Score = new Bindable<TScoreInfo?>();
private readonly Container scoreContainer;
private readonly Func<TScoreInfo, LeaderboardScore> createScoreDelegate;
protected override bool StartHidden => true;
private CancellationTokenSource? loadScoreCancellation;
public UserTopScoreContainer(Func<TScoreInfo, LeaderboardScore> createScoreDelegate)
{
this.createScoreDelegate = createScoreDelegate;
@ -65,9 +65,7 @@ namespace osu.Game.Online.Leaderboards
Score.BindValueChanged(onScoreChanged);
}
private CancellationTokenSource loadScoreCancellation;
private void onScoreChanged(ValueChangedEvent<TScoreInfo> score)
private void onScoreChanged(ValueChangedEvent<TScoreInfo?> score)
{
var newScore = score.NewValue;

View File

@ -24,6 +24,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Screens;
@ -187,7 +188,8 @@ namespace osu.Game
{
this.args = args;
forwardLoggedErrorsToNotifications();
forwardGeneralLogsToNotifications();
forwardTabletLogsToNotifications();
SentryLogger = new SentryLogger(this);
}
@ -994,7 +996,7 @@ namespace osu.Game
overlay.Depth = (float)-Clock.CurrentTime;
}
private void forwardLoggedErrorsToNotifications()
private void forwardGeneralLogsToNotifications()
{
int recentLogCount = 0;
@ -1002,7 +1004,7 @@ namespace osu.Game
Logger.NewEntry += entry =>
{
if (entry.Level < LogLevel.Important || entry.Target == null) return;
if (entry.Level < LogLevel.Important || entry.Target > LoggingTarget.Database) return;
const int short_term_display_limit = 3;
@ -1035,6 +1037,52 @@ namespace osu.Game
};
}
private void forwardTabletLogsToNotifications()
{
const string tablet_prefix = @"[Tablet] ";
bool notifyOnWarning = true;
Logger.NewEntry += entry =>
{
if (entry.Level < LogLevel.Important || entry.Target != LoggingTarget.Input || !entry.Message.StartsWith(tablet_prefix, StringComparison.OrdinalIgnoreCase))
return;
string message = entry.Message.Replace(tablet_prefix, string.Empty);
if (entry.Level == LogLevel.Error)
{
Schedule(() => Notifications.Post(new SimpleNotification
{
Text = $"Encountered tablet error: \"{message}\"",
Icon = FontAwesome.Solid.PenSquare,
IconColour = Colours.RedDark,
}));
}
else if (notifyOnWarning)
{
Schedule(() => Notifications.Post(new SimpleNotification
{
Text = @"Encountered tablet warning, your tablet may not function correctly. Click here for a list of all tablets supported.",
Icon = FontAwesome.Solid.PenSquare,
IconColour = Colours.YellowDark,
Activated = () =>
{
OpenUrlExternally("https://opentabletdriver.net/Tablets", true);
return true;
}
}));
notifyOnWarning = false;
}
};
Schedule(() =>
{
ITabletHandler tablet = Host.AvailableInputHandlers.OfType<ITabletHandler>().SingleOrDefault();
tablet?.Tablet.BindValueChanged(_ => notifyOnWarning = true, true);
});
}
private Task asyncLoadStream;
/// <summary>

View File

@ -125,6 +125,8 @@ namespace osu.Game
protected SessionStatics SessionStatics { get; private set; }
protected OsuColour Colours { get; private set; }
protected BeatmapManager BeatmapManager { get; private set; }
protected BeatmapModelDownloader BeatmapDownloader { get; private set; }
@ -311,7 +313,7 @@ namespace osu.Game
dependencies.CacheAs(powerStatus);
dependencies.Cache(SessionStatics = new SessionStatics());
dependencies.Cache(new OsuColour());
dependencies.Cache(Colours = new OsuColour());
RegisterImportHandler(BeatmapManager);
RegisterImportHandler(ScoreManager);

View File

@ -123,7 +123,7 @@ namespace osu.Game.Overlays.FirstRunSetup
beatmapSubscription?.Dispose();
}
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error)
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error) => Schedule(() =>
{
currentlyLoadedBeatmaps.Text = FirstRunSetupBeatmapScreenStrings.CurrentlyLoadedBeatmaps(sender.Count);
@ -139,7 +139,7 @@ namespace osu.Game.Overlays.FirstRunSetup
currentlyLoadedBeatmaps.ScaleTo(1.1f)
.ScaleTo(1, 1500, Easing.OutQuint);
}
}
});
private void downloadTutorial()
{

View File

@ -8,6 +8,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
@ -24,7 +25,7 @@ namespace osu.Game.Overlays.Music
public class PlaylistOverlay : VisibilityContainer
{
private const float transition_duration = 600;
private const float playlist_height = 510;
public const float PLAYLIST_HEIGHT = 510;
private readonly BindableList<Live<BeatmapSetInfo>> beatmapSets = new BindableList<Live<BeatmapSetInfo>>();
@ -130,7 +131,7 @@ namespace osu.Game.Overlays.Music
filter.Search.HoldFocus = true;
Schedule(() => filter.Search.TakeFocus());
this.ResizeTo(new Vector2(1, playlist_height), transition_duration, Easing.OutQuint);
this.ResizeTo(new Vector2(1, RelativeSizeAxes.HasFlagFast(Axes.Y) ? 1f : PLAYLIST_HEIGHT), transition_duration, Easing.OutQuint);
this.FadeIn(transition_duration, Easing.OutQuint);
}

View File

@ -58,12 +58,11 @@ namespace osu.Game.Overlays
[Resolved]
private RealmAccess realm { get; set; }
[BackgroundDependencyLoader]
private void load()
protected override void LoadComplete()
{
// Todo: These binds really shouldn't be here, but are unlikely to cause any issues for now.
// They are placed here for now since some tests rely on setting the beatmap _and_ their hierarchies inside their load(), which runs before the MusicController's load().
beatmap.BindValueChanged(beatmapChanged, true);
base.LoadComplete();
beatmap.BindValueChanged(b => changeBeatmap(b.NewValue), true);
mods.BindValueChanged(_ => ResetTrackAdjustments(), true);
}
@ -263,8 +262,6 @@ namespace osu.Game.Overlays
private IQueryable<BeatmapSetInfo> getBeatmapSets() => realm.Realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending);
private void beatmapChanged(ValueChangedEvent<WorkingBeatmap> beatmap) => changeBeatmap(beatmap.NewValue);
private void changeBeatmap(WorkingBeatmap newWorking)
{
// This method can potentially be triggered multiple times as it is eagerly fired in next() / prev() to ensure correct execution order

View File

@ -113,9 +113,12 @@ namespace osu.Game.Overlays
if (enabled)
// we want a slight delay before toggling notifications on to avoid the user becoming overwhelmed.
notificationsEnabler = Scheduler.AddDelayed(() => processingPosts = true, State.Value == Visibility.Visible ? 0 : 100);
notificationsEnabler = Scheduler.AddDelayed(() => processingPosts = true, State.Value == Visibility.Visible ? 0 : 250);
else
{
processingPosts = false;
toastTray.FlushAllToasts();
}
}
protected override void LoadComplete()

View File

@ -68,6 +68,8 @@ namespace osu.Game.Overlays.Notifications
public virtual bool Read { get; set; }
protected virtual bool AllowFlingDismiss => true;
public new bool IsDragged => dragContainer.IsDragged;
protected virtual IconUsage CloseButtonIcon => FontAwesome.Solid.Check;
@ -315,7 +317,7 @@ namespace osu.Game.Overlays.Notifications
protected override void OnDragEnd(DragEndEvent e)
{
if (Rotation < -10 || velocity.X < -0.3f)
if (notification.AllowFlingDismiss && (Rotation < -10 || velocity.X < -0.3f))
notification.Close(true);
else if (X > 30 || velocity.X > 0.3f)
notification.ForwardToOverlay?.Invoke();

View File

@ -25,6 +25,8 @@ namespace osu.Game.Overlays.Notifications
public Func<bool>? CancelRequested { get; set; }
protected override bool AllowFlingDismiss => false;
/// <summary>
/// The function to post completion notifications back to.
/// </summary>

View File

@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
@ -41,6 +42,12 @@ namespace osu.Game.Overlays.Notifications
}
}
public ColourInfo IconColour
{
get => IconContent.Colour;
set => IconContent.Colour = value;
}
private TextFlowContainer? textDrawable;
private SpriteIcon? iconDrawable;

View File

@ -38,6 +38,7 @@ namespace osu.Game.Overlays
private const float transition_length = 800;
private const float progress_height = 10;
private const float bottom_black_area_height = 55;
private const float margin = 10;
private Drawable background;
private ProgressBar progressBar;
@ -53,6 +54,7 @@ namespace osu.Game.Overlays
private Container dragContainer;
private Container playerContainer;
private Container playlistContainer;
protected override string PopInSampleName => "UI/now-playing-pop-in";
protected override string PopOutSampleName => "UI/now-playing-pop-out";
@ -69,7 +71,7 @@ namespace osu.Game.Overlays
public NowPlayingOverlay()
{
Width = 400;
Margin = new MarginPadding(10);
Margin = new MarginPadding(margin);
}
[BackgroundDependencyLoader]
@ -82,7 +84,6 @@ namespace osu.Game.Overlays
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
playerContainer = new Container
@ -182,8 +183,13 @@ namespace osu.Game.Overlays
}
},
},
playlistContainer = new Container
{
RelativeSizeAxes = Axes.X,
Y = player_height + margin,
}
}
}
},
};
}
@ -193,11 +199,10 @@ namespace osu.Game.Overlays
{
LoadComponentAsync(playlist = new PlaylistOverlay
{
RelativeSizeAxes = Axes.X,
Y = player_height + 10,
RelativeSizeAxes = Axes.Both,
}, _ =>
{
dragContainer.Add(playlist);
playlistContainer.Add(playlist);
playlist.State.BindValueChanged(s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint), true);
@ -242,7 +247,18 @@ namespace osu.Game.Overlays
{
base.UpdateAfterChildren();
Height = dragContainer.Height;
playlistContainer.Height = MathF.Min(Parent.DrawHeight - margin * 3 - player_height, PlaylistOverlay.PLAYLIST_HEIGHT);
float height = player_height;
if (playlist != null)
{
height += playlist.DrawHeight;
if (playlist.State.Value == Visibility.Visible)
height += margin;
}
Height = dragContainer.Height = height;
}
protected override void Update()

View File

@ -38,6 +38,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
Current = config.GetBindable<bool>(OsuSetting.KeyOverlay),
Keywords = new[] { "counter" },
},
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.AlwaysShowGameplayLeaderboard,
Current = config.GetBindable<bool>(OsuSetting.GameplayLeaderboard),
},
};
}
}

View File

@ -215,21 +215,21 @@ namespace osu.Game.Overlays.Settings.Sections.Input
rotation.BindTo(tabletHandler.Rotation);
areaOffset.BindTo(tabletHandler.AreaOffset);
areaOffset.BindValueChanged(val =>
areaOffset.BindValueChanged(val => Schedule(() =>
{
offsetX.Value = val.NewValue.X;
offsetY.Value = val.NewValue.Y;
}, true);
}), true);
offsetX.BindValueChanged(val => areaOffset.Value = new Vector2(val.NewValue, areaOffset.Value.Y));
offsetY.BindValueChanged(val => areaOffset.Value = new Vector2(areaOffset.Value.X, val.NewValue));
areaSize.BindTo(tabletHandler.AreaSize);
areaSize.BindValueChanged(val =>
areaSize.BindValueChanged(val => Schedule(() =>
{
sizeX.Value = val.NewValue.X;
sizeY.Value = val.NewValue.Y;
}, true);
}), true);
sizeX.BindValueChanged(val =>
{
@ -255,7 +255,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
});
tablet.BindTo(tabletHandler.Tablet);
tablet.BindValueChanged(val =>
tablet.BindValueChanged(val => Schedule(() =>
{
Scheduler.AddOnce(updateVisibility);
@ -274,7 +274,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
sizeY.Default = sizeY.MaxValue = tab.Size.Y;
areaSize.Default = new Vector2(sizeX.Default, sizeY.Default);
}, true);
}), true);
}
private void updateVisibility()

View File

@ -58,6 +58,9 @@ namespace osu.Game.Rulesets.Configuration
pendingWrites.Clear();
}
if (!changed.Any())
return true;
realm?.Write(r =>
{
foreach (var c in changed)

View File

@ -36,32 +36,24 @@ namespace osu.Game.Rulesets.Mods
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp), typeof(ModAutoplay) };
[SettingSource("Initial rate", "The starting speed of the track")]
public BindableNumber<double> InitialRate { get; } = new BindableDouble
public BindableNumber<double> InitialRate { get; } = new BindableDouble(1)
{
MinValue = 0.5,
MaxValue = 2,
Default = 1,
Value = 1,
Precision = 0.01
};
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public BindableBool AdjustPitch { get; } = new BindableBool
{
Default = true,
Value = true
};
public BindableBool AdjustPitch { get; } = new BindableBool(true);
/// <summary>
/// The instantaneous rate of the track.
/// Every frame this mod will attempt to smoothly adjust this to meet <see cref="targetRate"/>.
/// </summary>
public BindableNumber<double> SpeedChange { get; } = new BindableDouble
public BindableNumber<double> SpeedChange { get; } = new BindableDouble(1)
{
MinValue = min_allowable_rate,
MaxValue = max_allowable_rate,
Default = 1,
Value = 1
};
// The two constants below denote the maximum allowable range of rates that `SpeedChange` can take.

View File

@ -18,12 +18,10 @@ namespace osu.Game.Rulesets.Mods
public override LocalisableString Description => "Zoooooooooom...";
[SettingSource("Speed increase", "The actual increase to apply")]
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(1.5)
{
MinValue = 1.01,
MaxValue = 2,
Default = 1.5,
Value = 1.5,
Precision = 0.01,
};
}

View File

@ -18,12 +18,10 @@ namespace osu.Game.Rulesets.Mods
public override LocalisableString Description => "Less zoom...";
[SettingSource("Speed decrease", "The actual decrease to apply")]
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(0.75)
{
MinValue = 0.5,
MaxValue = 0.99,
Default = 0.75,
Value = 0.75,
Precision = 0.01,
};
}

View File

@ -36,34 +36,20 @@ namespace osu.Game.Rulesets.Mods
private readonly BindableNumber<int> currentCombo = new BindableInt();
[SettingSource("Enable metronome", "Add a metronome beat to help you keep track of the rhythm.")]
public BindableBool EnableMetronome { get; } = new BindableBool
{
Default = true,
Value = true
};
public BindableBool EnableMetronome { get; } = new BindableBool(true);
[SettingSource("Final volume at combo", "The combo count at which point the track reaches its final volume.", SettingControlType = typeof(SettingsSlider<int, MuteComboSlider>))]
public BindableInt MuteComboCount { get; } = new BindableInt
public BindableInt MuteComboCount { get; } = new BindableInt(100)
{
Default = 100,
Value = 100,
MinValue = 0,
MaxValue = 500,
};
[SettingSource("Start muted", "Increase volume as combo builds.")]
public BindableBool InverseMuting { get; } = new BindableBool
{
Default = false,
Value = false
};
public BindableBool InverseMuting { get; } = new BindableBool();
[SettingSource("Mute hit sounds", "Hit sounds are also muted alongside the track.")]
public BindableBool AffectsHitSounds { get; } = new BindableBool
{
Default = true,
Value = true
};
public BindableBool AffectsHitSounds { get; } = new BindableBool(true);
protected ModMuted()
{

View File

@ -6,7 +6,9 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
@ -34,6 +36,11 @@ namespace osu.Game.Rulesets.Mods
protected float ComboBasedAlpha;
[SettingSource(
"Hidden at combo",
"The combo count at which the cursor becomes completely hidden",
SettingControlType = typeof(SettingsSlider<int, HiddenComboSlider>)
)]
public abstract BindableInt HiddenComboCount { get; }
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;

View File

@ -18,10 +18,6 @@ namespace osu.Game.Rulesets.Mods
public override double ScoreMultiplier => 1;
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
public Bindable<int?> Seed { get; } = new Bindable<int?>
{
Default = null,
Value = null
};
public Bindable<int?> Seed { get; } = new Bindable<int?>();
}
}

View File

@ -39,10 +39,8 @@ namespace osu.Game.Rulesets.Mods
private double finalRateTime;
private double beginRampTime;
public BindableNumber<double> SpeedChange { get; } = new BindableDouble
public BindableNumber<double> SpeedChange { get; } = new BindableDouble(1)
{
Default = 1,
Value = 1,
Precision = 0.01,
};

View File

@ -6,7 +6,6 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
namespace osu.Game.Rulesets.Mods
{
@ -17,32 +16,21 @@ namespace osu.Game.Rulesets.Mods
public override LocalisableString Description => "Sloooow doooown...";
public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleDown;
[SettingSource("Initial rate", "The starting speed of the track")]
public override BindableNumber<double> InitialRate { get; } = new BindableDouble
public override BindableNumber<double> InitialRate { get; } = new BindableDouble(1)
{
MinValue = 0.51,
MaxValue = 2,
Default = 1,
Value = 1,
Precision = 0.01,
};
[SettingSource("Final rate", "The speed increase to ramp towards")]
public override BindableNumber<double> FinalRate { get; } = new BindableDouble
public override BindableNumber<double> FinalRate { get; } = new BindableDouble(0.75)
{
MinValue = 0.5,
MaxValue = 1.99,
Default = 0.75,
Value = 0.75,
Precision = 0.01,
};
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public override BindableBool AdjustPitch { get; } = new BindableBool
{
Default = true,
Value = true
};
public override BindableBool AdjustPitch { get; } = new BindableBool(true);
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindUp)).ToArray();

View File

@ -6,7 +6,6 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
namespace osu.Game.Rulesets.Mods
{
@ -17,32 +16,21 @@ namespace osu.Game.Rulesets.Mods
public override LocalisableString Description => "Can you keep up?";
public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleUp;
[SettingSource("Initial rate", "The starting speed of the track")]
public override BindableNumber<double> InitialRate { get; } = new BindableDouble
public override BindableNumber<double> InitialRate { get; } = new BindableDouble(1)
{
MinValue = 0.5,
MaxValue = 1.99,
Default = 1,
Value = 1,
Precision = 0.01,
};
[SettingSource("Final rate", "The speed increase to ramp towards")]
public override BindableNumber<double> FinalRate { get; } = new BindableDouble
public override BindableNumber<double> FinalRate { get; } = new BindableDouble(1.5)
{
MinValue = 0.51,
MaxValue = 2,
Default = 1.5,
Value = 1.5,
Precision = 0.01,
};
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public override BindableBool AdjustPitch { get; } = new BindableBool
{
Default = true,
Value = true
};
public override BindableBool AdjustPitch { get; } = new BindableBool(true);
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindDown)).ToArray();

View File

@ -651,7 +651,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <remarks>
/// This does not affect the time offset provided to invocations of <see cref="CheckForResult"/>.
/// </remarks>
protected virtual double MaximumJudgementOffset => HitObject.HitWindows?.WindowFor(HitResult.Miss) ?? 0;
public virtual double MaximumJudgementOffset => HitObject.HitWindows?.WindowFor(HitResult.Miss) ?? 0;
/// <summary>
/// Applies the <see cref="Result"/> of this <see cref="DrawableHitObject"/>, notifying responders such as

View File

@ -60,7 +60,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
/// </summary>
protected readonly BindableDouble TimeRange = new BindableDouble(time_span_default)
{
Default = time_span_default,
MinValue = time_span_min,
MaxValue = time_span_max
};

View File

@ -3,6 +3,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using osu.Framework.Allocation;
@ -214,7 +215,10 @@ namespace osu.Game.Rulesets.UI.Scrolling
break;
}
return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength);
double computedStartTime = scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength);
// always load the hitobject before its first judgement offset
return Math.Min(hitObject.HitObject.StartTime - hitObject.MaximumJudgementOffset, computedStartTime);
}
private void updateLayoutRecursive(DrawableHitObject hitObject)

View File

@ -4,11 +4,12 @@
#nullable disable
using System;
using System.Linq;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
@ -34,8 +35,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved]
private OsuColour colours { get; set; }
private static readonly int highest_divisor = BindableBeatDivisor.PREDEFINED_DIVISORS.Last();
public TimelineTickDisplay()
{
RelativeSizeAxes = Axes.Both;
@ -80,20 +79,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
base.Update();
if (timeline != null)
if (timeline == null || DrawWidth <= 0) return;
(float, float) newRange = (
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X,
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X);
if (visibleRange != newRange)
{
var newRange = (
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X,
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X);
visibleRange = newRange;
if (visibleRange != newRange)
{
visibleRange = newRange;
// actual regeneration only needs to occur if we've passed one of the known next min/max tick boundaries.
if (nextMinTick == null || nextMaxTick == null || (visibleRange.min < nextMinTick || visibleRange.max > nextMaxTick))
tickCache.Invalidate();
}
// actual regeneration only needs to occur if we've passed one of the known next min/max tick boundaries.
if (nextMinTick == null || nextMaxTick == null || (visibleRange.min < nextMinTick || visibleRange.max > nextMaxTick))
tickCache.Invalidate();
}
if (!tickCache.IsValid)
@ -151,6 +149,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
if (Children.Count > 512)
{
// There should always be a sanely small number of ticks rendered.
// If this assertion triggers, either the zoom logic is broken or a beatmap is
// probably doing weird things...
//
// Let's hope the latter never happens.
// If it does, we can choose to either fix it or ignore it as an outlier.
string message = $"Timeline is rendering many ticks ({Children.Count})";
Logger.Log(message);
Debug.Fail(message);
}
int usedDrawables = drawableIndex;
// save a few drawables beyond the currently used for edge cases.

View File

@ -56,7 +56,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected ZoomableScrollContainer()
: base(Direction.Horizontal)
{
base.Content.Add(zoomedContent = new Container { RelativeSizeAxes = Axes.Y });
base.Content.Add(zoomedContent = new Container
{
RelativeSizeAxes = Axes.Y,
// We must hide content until SetupZoom is called.
// If not, a child component that relies on its DrawWidth (via RelativeSizeAxes) may see a very incorrect value
// momentarily, as noticed in the TimelineTickDisplay, which would render thousands of ticks incorrectly.
Alpha = 0,
});
AddLayout(zoomedContentWidthCache);
}
@ -94,6 +101,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
maxZoom = maximum;
CurrentZoom = zoomTarget = initial;
isZoomSetUp = true;
zoomedContent.Show();
}
/// <summary>
@ -118,9 +127,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
CurrentZoom = zoomTarget = newZoom;
}
protected override void Update()
protected override void UpdateAfterChildren()
{
base.Update();
base.UpdateAfterChildren();
if (!zoomedContentWidthCache.IsValid)
updateZoomedContentWidth();

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.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -16,7 +14,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
public class MatchLeaderboard : Leaderboard<MatchLeaderboardScope, APIUserScoreAggregate>
{
[Resolved(typeof(Room), nameof(Room.RoomID))]
private Bindable<long?> roomId { get; set; }
private Bindable<long?> roomId { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
@ -33,7 +31,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
protected override bool IsOnlineScope => true;
protected override APIRequest FetchScores(CancellationToken cancellationToken)
protected override APIRequest? FetchScores(CancellationToken cancellationToken)
{
if (roomId.Value == null)
return null;

View File

@ -54,6 +54,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
private const float disabled_alpha = 0.2f;
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
public Action? SettingsApplied;
public OsuTextBox NameField = null!;
@ -424,7 +426,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void hideError() => ErrorText.FadeOut(50);
private void onSuccess(Room room)
private void onSuccess(Room room) => Schedule(() =>
{
Debug.Assert(applyingSettingsOperation != null);
@ -432,9 +434,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
applyingSettingsOperation.Dispose();
applyingSettingsOperation = null;
}
});
private void onError(string text)
private void onError(string text) => Schedule(() =>
{
Debug.Assert(applyingSettingsOperation != null);
@ -455,7 +457,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
applyingSettingsOperation.Dispose();
applyingSettingsOperation = null;
}
});
}
public class CreateOrUpdateButton : TriangleButton

View File

@ -9,8 +9,6 @@ using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Graphics.UserInterface;
@ -21,7 +19,6 @@ using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Ranking;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
@ -41,14 +38,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private readonly TaskCompletionSource<bool> resultsReady = new TaskCompletionSource<bool>();
private MultiplayerGameplayLeaderboard leaderboard;
private readonly MultiplayerRoomUser[] users;
private readonly Bindable<bool> leaderboardExpanded = new BindableBool();
private LoadingLayer loadingDisplay;
private FillFlowContainer leaderboardFlow;
private MultiplayerGameplayLeaderboard multiplayerLeaderboard;
/// <summary>
/// Construct a multiplayer player.
@ -62,7 +56,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
AllowPause = false,
AllowRestart = false,
AllowSkipping = room.AutoSkip.Value,
AutomaticallySkipIntro = room.AutoSkip.Value
AutomaticallySkipIntro = room.AutoSkip.Value,
AlwaysShowLeaderboard = true,
})
{
this.users = users;
@ -74,45 +69,33 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (!LoadedBeatmapSuccessfully)
return;
HUDOverlay.Add(leaderboardFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5)
});
HUDOverlay.HoldingForHUD.BindValueChanged(_ => updateLeaderboardExpandedState());
LocalUserPlaying.BindValueChanged(_ => updateLeaderboardExpandedState(), true);
// todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area.
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(users), l =>
{
if (!LoadedBeatmapSuccessfully)
return;
leaderboard.Expanded.BindTo(leaderboardExpanded);
leaderboardFlow.Insert(0, l);
if (leaderboard.TeamScores.Count >= 2)
{
LoadComponentAsync(new GameplayMatchScoreDisplay
{
Team1Score = { BindTarget = leaderboard.TeamScores.First().Value },
Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value },
Expanded = { BindTarget = HUDOverlay.ShowHud },
}, scoreDisplay => leaderboardFlow.Insert(1, scoreDisplay));
}
});
LoadComponentAsync(new GameplayChatDisplay(Room)
{
Expanded = { BindTarget = leaderboardExpanded },
}, chat => leaderboardFlow.Insert(2, chat));
Expanded = { BindTarget = LeaderboardExpandedState },
}, chat => HUDOverlay.LeaderboardFlow.Insert(2, chat));
HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue });
}
protected override GameplayLeaderboard CreateGameplayLeaderboard() => multiplayerLeaderboard = new MultiplayerGameplayLeaderboard(users);
protected override void AddLeaderboardToHUD(GameplayLeaderboard leaderboard)
{
Debug.Assert(leaderboard == multiplayerLeaderboard);
HUDOverlay.LeaderboardFlow.Insert(0, leaderboard);
if (multiplayerLeaderboard.TeamScores.Count >= 2)
{
LoadComponentAsync(new GameplayMatchScoreDisplay
{
Team1Score = { BindTarget = multiplayerLeaderboard.TeamScores.First().Value },
Team2Score = { BindTarget = multiplayerLeaderboard.TeamScores.Last().Value },
Expanded = { BindTarget = HUDOverlay.ShowHud },
}, scoreDisplay => HUDOverlay.LeaderboardFlow.Insert(1, scoreDisplay));
}
}
protected override void LoadAsyncComplete()
{
base.LoadAsyncComplete();
@ -167,9 +150,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
}
private void updateLeaderboardExpandedState() =>
leaderboardExpanded.Value = !LocalUserPlaying.Value || HUDOverlay.HoldingForHUD.Value;
private void failAndBail(string message = null)
{
if (!string.IsNullOrEmpty(message))
@ -178,23 +158,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Schedule(() => PerformExit(false));
}
protected override void Update()
{
base.Update();
if (!LoadedBeatmapSuccessfully)
return;
adjustLeaderboardPosition();
}
private void adjustLeaderboardPosition()
{
const float padding = 44; // enough margin to avoid the hit error display.
leaderboardFlow.Position = new Vector2(padding, padding + HUDOverlay.TopScoringElementsHeight);
}
private void onGameplayStarted() => Scheduler.Add(() =>
{
if (!this.IsCurrentScreen())
@ -232,8 +195,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
Debug.Assert(Room.RoomID.Value != null);
return leaderboard.TeamScores.Count == 2
? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem, leaderboard.TeamScores)
return multiplayerLeaderboard.TeamScores.Count == 2
? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem, multiplayerLeaderboard.TeamScores)
: new MultiplayerResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem);
}

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.Timing;
using osu.Game.Online.Multiplayer;

View File

@ -105,7 +105,8 @@ namespace osu.Game.Screens.OnlinePlay
while (this.IsCurrentScreen())
this.Exit();
}
else
// Also handle the case where a child screen is current (ie. gameplay).
else if (this.GetChildScreen() != null)
{
this.MakeCurrent();
Schedule(forcefullyExit);

View File

@ -1,11 +1,8 @@
// 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 JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Extensions.Color4Extensions;
@ -13,15 +10,14 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Users;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
{
public class GameplayLeaderboard : CompositeDrawable
public abstract class GameplayLeaderboard : CompositeDrawable
{
private readonly int maxPanels;
private readonly Cached sorting = new Cached();
public Bindable<bool> Expanded = new Bindable<bool>();
@ -31,22 +27,22 @@ namespace osu.Game.Screens.Play.HUD
private bool requiresScroll;
private readonly OsuScrollContainer scroll;
private GameplayLeaderboardScore trackedScore;
public GameplayLeaderboardScore? TrackedScore { get; private set; }
private const int max_panels = 8;
/// <summary>
/// Create a new leaderboard.
/// </summary>
/// <param name="maxPanels">The maximum panels to show at once. Defines the maximum height of this component.</param>
public GameplayLeaderboard(int maxPanels = 8)
protected GameplayLeaderboard()
{
this.maxPanels = maxPanels;
Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH;
InternalChildren = new Drawable[]
{
scroll = new InputDisabledScrollContainer
{
ClampExtension = 0,
RelativeSizeAxes = Axes.Both,
Child = Flow = new FillFlowContainer<GameplayLeaderboardScore>
{
@ -77,33 +73,26 @@ namespace osu.Game.Screens.Play.HUD
/// Whether the player should be tracked on the leaderboard.
/// Set to <c>true</c> for the local player or a player whose replay is currently being played.
/// </param>
public ILeaderboardScore Add([CanBeNull] APIUser user, bool isTracked)
public ILeaderboardScore Add(IUser? user, bool isTracked)
{
var drawable = CreateLeaderboardScoreDrawable(user, isTracked);
if (isTracked)
{
if (trackedScore != null)
if (TrackedScore != null)
throw new InvalidOperationException("Cannot track more than one score.");
trackedScore = drawable;
TrackedScore = drawable;
}
drawable.Expanded.BindTo(Expanded);
Flow.Add(drawable);
drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true);
drawable.DisplayOrder.BindValueChanged(_ => sorting.Invalidate(), true);
int displayCount = Math.Min(Flow.Count, maxPanels);
int displayCount = Math.Min(Flow.Count, max_panels);
Height = displayCount * (GameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y);
// Add extra margin space to flow equal to height of leaderboard.
// This ensures the content is always on screen, but also accounts for the fact that scroll operations
// without animation were actually forcing the local score to a location it can't usually reside at.
//
// Basically, the local score was in the scroll extension region (due to always trying to scroll the
// local player to the middle of the display, but there being no other content below the local player
// to scroll up by).
Flow.Margin = new MarginPadding { Bottom = Height };
requiresScroll = displayCount != Flow.Count;
return drawable;
@ -112,20 +101,21 @@ namespace osu.Game.Screens.Play.HUD
public void Clear()
{
Flow.Clear();
trackedScore = null;
TrackedScore = null;
scroll.ScrollToStart(false);
}
protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(APIUser user, bool isTracked) =>
protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser? user, bool isTracked) =>
new GameplayLeaderboardScore(user, isTracked);
protected override void Update()
{
base.Update();
if (requiresScroll && trackedScore != null)
if (requiresScroll && TrackedScore != null)
{
float scrollTarget = scroll.GetChildPosInContent(trackedScore) + trackedScore.DrawHeight / 2 - scroll.DrawHeight / 2;
float scrollTarget = scroll.GetChildPosInContent(TrackedScore) + TrackedScore.DrawHeight / 2 - scroll.DrawHeight / 2;
scroll.ScrollTo(scrollTarget);
}
@ -173,7 +163,10 @@ namespace osu.Game.Screens.Play.HUD
if (sorting.IsValid)
return;
var orderedByScore = Flow.OrderByDescending(i => i.TotalScore.Value).ToList();
var orderedByScore = Flow
.OrderByDescending(i => i.TotalScore.Value)
.ThenBy(i => i.DisplayOrder.Value)
.ToList();
for (int i = 0; i < Flow.Count; i++)
{

View File

@ -12,7 +12,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osu.Game.Utils;
using osuTK;
@ -55,6 +55,7 @@ namespace osu.Game.Screens.Play.HUD
public BindableDouble Accuracy { get; } = new BindableDouble(1);
public BindableInt Combo { get; } = new BindableInt();
public BindableBool HasQuit { get; } = new BindableBool();
public Bindable<long> DisplayOrder { get; } = new Bindable<long>();
public Color4? BackgroundColour { get; set; }
@ -81,7 +82,7 @@ namespace osu.Game.Screens.Play.HUD
}
[CanBeNull]
public APIUser User { get; }
public IUser User { get; }
/// <summary>
/// Whether this score is the local user or a replay player (and should be focused / always visible).
@ -103,7 +104,7 @@ namespace osu.Game.Screens.Play.HUD
/// </summary>
/// <param name="user">The score's player.</param>
/// <param name="tracked">Whether the player is the local user or a replay player.</param>
public GameplayLeaderboardScore([CanBeNull] APIUser user, bool tracked)
public GameplayLeaderboardScore([CanBeNull] IUser user, bool tracked)
{
User = user;
Tracked = tracked;

View File

@ -14,5 +14,11 @@ namespace osu.Game.Screens.Play.HUD
BindableInt Combo { get; }
BindableBool HasQuit { get; }
/// <summary>
/// An optional value to guarantee stable ordering.
/// Lower numbers will appear higher in cases of <see cref="TotalScore"/> ties.
/// </summary>
Bindable<long> DisplayOrder { get; }
}
}

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.Collections.Generic;
using System.Collections.Specialized;
@ -12,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics;
@ -21,6 +20,7 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Scoring;
using osu.Game.Users;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
@ -33,19 +33,20 @@ namespace osu.Game.Screens.Play.HUD
public readonly SortedDictionary<int, BindableLong> TeamScores = new SortedDictionary<int, BindableLong>();
[Resolved]
private OsuColour colours { get; set; }
private OsuColour colours { get; set; } = null!;
[Resolved]
private SpectatorClient spectatorClient { get; set; }
private SpectatorClient spectatorClient { get; set; } = null!;
[Resolved]
private MultiplayerClient multiplayerClient { get; set; }
private MultiplayerClient multiplayerClient { get; set; } = null!;
[Resolved]
private UserLookupCache userLookupCache { get; set; }
private UserLookupCache userLookupCache { get; set; } = null!;
private Bindable<ScoringMode> scoringMode = null!;
private readonly MultiplayerRoomUser[] playingUsers;
private Bindable<ScoringMode> scoringMode;
private readonly IBindableList<int> playingUserIds = new BindableList<int>();
@ -125,14 +126,17 @@ namespace osu.Game.Screens.Play.HUD
playingUserIds.BindCollectionChanged(playingUsersChanged);
}
protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(APIUser user, bool isTracked)
protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser? user, bool isTracked)
{
var leaderboardScore = base.CreateLeaderboardScoreDrawable(user, isTracked);
if (UserScores[user.Id].Team is int team)
if (user != null)
{
leaderboardScore.BackgroundColour = getTeamColour(team).Lighten(1.2f);
leaderboardScore.TextColour = Color4.White;
if (UserScores[user.OnlineID].Team is int team)
{
leaderboardScore.BackgroundColour = getTeamColour(team).Lighten(1.2f);
leaderboardScore.TextColour = Color4.White;
}
}
return leaderboardScore;
@ -188,7 +192,7 @@ namespace osu.Game.Screens.Play.HUD
{
base.Dispose(isDisposing);
if (spectatorClient != null)
if (spectatorClient.IsNotNull())
{
foreach (var user in playingUsers)
spectatorClient.StopWatchingUser(user.UserID);

View File

@ -0,0 +1,99 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Screens.Play.HUD
{
public class SoloGameplayLeaderboard : GameplayLeaderboard
{
private const int duration = 100;
private readonly Bindable<bool> configVisibility = new Bindable<bool>();
private readonly IUser trackingUser;
public readonly IBindableList<ScoreInfo> Scores = new BindableList<ScoreInfo>();
// hold references to ensure bindables are updated.
private readonly List<Bindable<long>> scoreBindables = new List<Bindable<long>>();
[Resolved]
private ScoreProcessor scoreProcessor { get; set; } = null!;
[Resolved]
private ScoreManager scoreManager { get; set; } = null!;
/// <summary>
/// Whether the leaderboard should be visible regardless of the configuration value.
/// This is true by default, but can be changed.
/// </summary>
public readonly Bindable<bool> AlwaysVisible = new Bindable<bool>(true);
public SoloGameplayLeaderboard(IUser trackingUser)
{
this.trackingUser = trackingUser;
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility);
}
protected override void LoadComplete()
{
base.LoadComplete();
Scores.BindCollectionChanged((_, _) => Scheduler.AddOnce(showScores), true);
// Alpha will be updated via `updateVisibility` below.
Alpha = 0;
AlwaysVisible.BindValueChanged(_ => updateVisibility());
configVisibility.BindValueChanged(_ => updateVisibility(), true);
}
private void showScores()
{
Clear();
scoreBindables.Clear();
if (!Scores.Any())
return;
foreach (var s in Scores)
{
var score = Add(s.User, false);
var bindableTotal = scoreManager.GetBindableTotalScore(s);
// Direct binding not possible due to differing types (see https://github.com/ppy/osu/issues/20298).
bindableTotal.BindValueChanged(total => score.TotalScore.Value = total.NewValue, true);
scoreBindables.Add(bindableTotal);
score.Accuracy.Value = s.Accuracy;
score.Combo.Value = s.MaxCombo;
score.DisplayOrder.Value = s.OnlineID > 0 ? s.OnlineID : s.Date.ToUnixTimeSeconds();
}
ILeaderboardScore local = Add(trackingUser, true);
local.TotalScore.BindTarget = scoreProcessor.TotalScore;
local.Accuracy.BindTarget = scoreProcessor.Accuracy;
local.Combo.BindTarget = scoreProcessor.HighestCombo;
// Local score should always show lower than any existing scores in cases of ties.
local.DisplayOrder.Value = long.MaxValue;
}
private void updateVisibility() =>
this.FadeTo(AlwaysVisible.Value || configVisibility.Value ? 1 : 0, duration);
}
}

View File

@ -9,7 +9,6 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
@ -35,11 +34,6 @@ namespace osu.Game.Screens.Play
public const Easing FADE_EASING = Easing.OutQuint;
/// <summary>
/// The total height of all the top of screen scoring elements.
/// </summary>
public float TopScoringElementsHeight { get; private set; }
/// <summary>
/// The total height of all the bottom of screen scoring elements.
/// </summary>
@ -80,9 +74,15 @@ namespace osu.Game.Screens.Play
private readonly SkinnableTargetContainer mainComponents;
private IEnumerable<Drawable> hideTargets => new Drawable[] { mainComponents, KeyCounter, topRightElements };
/// <summary>
/// A flow which sits at the left side of the screen to house leaderboard (and related) components.
/// Will automatically be positioned to avoid colliding with top scoring elements.
/// </summary>
public readonly FillFlowContainer LeaderboardFlow;
public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods)
private readonly List<Drawable> hideTargets;
public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, bool alwaysShowLeaderboard = true)
{
this.drawableRuleset = drawableRuleset;
this.mods = mods;
@ -127,8 +127,20 @@ namespace osu.Game.Screens.Play
HoldToQuit = CreateHoldForMenuButton(),
}
},
clicksPerSecondCalculator = new ClicksPerSecondCalculator()
LeaderboardFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Padding = new MarginPadding(44), // enough margin to avoid the hit error display
Spacing = new Vector2(5)
},
clicksPerSecondCalculator = new ClicksPerSecondCalculator(),
};
hideTargets = new List<Drawable> { mainComponents, KeyCounter, topRightElements };
if (!alwaysShowLeaderboard)
hideTargets.Add(LeaderboardFlow);
}
[BackgroundDependencyLoader(true)]
@ -177,22 +189,36 @@ namespace osu.Game.Screens.Play
{
base.Update();
Vector2? lowestTopScreenSpace = null;
float? lowestTopScreenSpaceLeft = null;
float? lowestTopScreenSpaceRight = null;
Vector2? highestBottomScreenSpace = null;
// LINQ cast can be removed when IDrawable interface includes Anchor / RelativeSizeAxes.
foreach (var element in mainComponents.Components.Cast<Drawable>())
{
// for now align top-right components with the bottom-edge of the lowest top-anchored hud element.
if (element.Anchor.HasFlagFast(Anchor.TopRight) || (element.Anchor.HasFlagFast(Anchor.y0) && element.RelativeSizeAxes == Axes.X))
// for now align some top components with the bottom-edge of the lowest top-anchored hud element.
if (element.Anchor.HasFlagFast(Anchor.y0))
{
// health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area.
if (element is LegacyHealthDisplay)
continue;
var bottomRight = element.ScreenSpaceDrawQuad.BottomRight;
if (lowestTopScreenSpace == null || bottomRight.Y > lowestTopScreenSpace.Value.Y)
lowestTopScreenSpace = bottomRight;
float bottom = element.ScreenSpaceDrawQuad.BottomRight.Y;
bool isRelativeX = element.RelativeSizeAxes == Axes.X;
if (element.Anchor.HasFlagFast(Anchor.TopRight) || isRelativeX)
{
if (lowestTopScreenSpaceRight == null || bottom > lowestTopScreenSpaceRight.Value)
lowestTopScreenSpaceRight = bottom;
}
if (element.Anchor.HasFlagFast(Anchor.TopLeft) || isRelativeX)
{
if (lowestTopScreenSpaceLeft == null || bottom > lowestTopScreenSpaceLeft.Value)
lowestTopScreenSpaceLeft = bottom;
}
}
// and align bottom-right components with the top-edge of the highest bottom-anchored hud element.
else if (element.Anchor.HasFlagFast(Anchor.BottomRight) || (element.Anchor.HasFlagFast(Anchor.y2) && element.RelativeSizeAxes == Axes.X))
@ -203,11 +229,16 @@ namespace osu.Game.Screens.Play
}
}
if (lowestTopScreenSpace.HasValue)
topRightElements.Y = TopScoringElementsHeight = MathHelper.Clamp(ToLocalSpace(lowestTopScreenSpace.Value).Y, 0, DrawHeight - topRightElements.DrawHeight);
if (lowestTopScreenSpaceRight.HasValue)
topRightElements.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - topRightElements.DrawHeight);
else
topRightElements.Y = 0;
if (lowestTopScreenSpaceLeft.HasValue)
LeaderboardFlow.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight);
else
LeaderboardFlow.Y = 0;
if (highestBottomScreenSpace.HasValue)
bottomRightElements.Y = BottomScoringElementsHeight = -MathHelper.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight);
else

View File

@ -39,6 +39,7 @@ namespace osu.Game.Screens.Play
{
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Alpha = 0,
};
}

View File

@ -34,7 +34,6 @@ namespace osu.Game.Screens.Play
public readonly BindableNumber<double> UserPlaybackRate = new BindableDouble(1)
{
Default = 1,
MinValue = 0.5,
MaxValue = 2,
Precision = 0.1,

View File

@ -9,6 +9,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@ -34,6 +35,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Ranking;
using osu.Game.Skinning;
using osu.Game.Users;
@ -375,6 +377,8 @@ namespace osu.Game.Screens.Play
if (Configuration.AutomaticallySkipIntro)
skipIntroOverlay.SkipWhenReady();
loadLeaderboard();
}
protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart);
@ -417,7 +421,7 @@ namespace osu.Game.Screens.Play
// display the cursor above some HUD elements.
DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),
DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(),
HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods)
HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration.AlwaysShowLeaderboard)
{
HoldToQuit =
{
@ -562,9 +566,6 @@ namespace osu.Game.Screens.Play
/// </param>
protected void PerformExit(bool showDialogFirst)
{
// if an exit has been requested, cancel any pending completion (the user has shown intention to exit).
resultsDisplayDelegate?.Cancel();
// there is a chance that an exit request occurs after the transition to results has already started.
// even in such a case, the user has shown intent, so forcefully return to this screen (to proceed with the upwards exit process).
if (!this.IsCurrentScreen())
@ -599,6 +600,9 @@ namespace osu.Game.Screens.Play
}
}
// if an exit has been requested, cancel any pending completion (the user has shown intention to exit).
resultsDisplayDelegate?.Cancel();
// The actual exit is performed if
// - the pause / fail dialog was not requested
// - the pause / fail dialog was requested but is already displayed (user showing intention to exit).
@ -776,19 +780,11 @@ namespace osu.Game.Screens.Play
/// </summary>
/// <remarks>
/// A final display will only occur once all work is completed in <see cref="PrepareScoreForResultsAsync"/>. This means that even after calling this method, the results screen will never be shown until <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="true"/>.
///
/// Calling this method multiple times will have no effect.
/// </remarks>
/// <param name="withDelay">Whether a minimum delay (<see cref="RESULTS_DISPLAY_DELAY"/>) should be added before the screen is displayed.</param>
private void progressToResults(bool withDelay)
{
if (resultsDisplayDelegate != null)
// Note that if progressToResults is called one withDelay=true and then withDelay=false, this no-delay timing will not be
// accounted for. shouldn't be a huge concern (a user pressing the skip button after a results progression has already been queued
// may take x00 more milliseconds than expected in the very rare edge case).
//
// If required we can handle this more correctly by rescheduling here.
return;
resultsDisplayDelegate?.Cancel();
double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0;
@ -820,6 +816,41 @@ namespace osu.Game.Screens.Play
return mouseWheelDisabled.Value && !e.AltPressed;
}
#region Gameplay leaderboard
protected readonly Bindable<bool> LeaderboardExpandedState = new BindableBool();
private void loadLeaderboard()
{
HUDOverlay.HoldingForHUD.BindValueChanged(_ => updateLeaderboardExpandedState());
LocalUserPlaying.BindValueChanged(_ => updateLeaderboardExpandedState(), true);
var gameplayLeaderboard = CreateGameplayLeaderboard();
if (gameplayLeaderboard != null)
{
LoadComponentAsync(gameplayLeaderboard, leaderboard =>
{
if (!LoadedBeatmapSuccessfully)
return;
leaderboard.Expanded.BindTo(LeaderboardExpandedState);
AddLeaderboardToHUD(leaderboard);
});
}
}
[CanBeNull]
protected virtual GameplayLeaderboard CreateGameplayLeaderboard() => null;
protected virtual void AddLeaderboardToHUD(GameplayLeaderboard leaderboard) => HUDOverlay.LeaderboardFlow.Add(leaderboard);
private void updateLeaderboardExpandedState() =>
LeaderboardExpandedState.Value = !LocalUserPlaying.Value || HUDOverlay.HoldingForHUD.Value;
#endregion
#region Fail Logic
protected FailOverlay FailOverlay { get; private set; }

View File

@ -36,5 +36,10 @@ namespace osu.Game.Screens.Play
/// Whether the intro should be skipped by default.
/// </summary>
public bool AutomaticallySkipIntro { get; set; }
/// <summary>
/// Whether the gameplay leaderboard should always be shown (usually in a contracted state).
/// </summary>
public bool AlwaysShowLeaderboard { get; set; }
}
}

View File

@ -31,8 +31,6 @@ namespace osu.Game.Screens.Play.PlayerSettings
public BindableDouble Current { get; } = new BindableDouble
{
Default = 0,
Value = 0,
MinValue = -50,
MaxValue = 50,
Precision = 0.1,

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