1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-15 01:23:44 +08:00

Merge branch 'master' into deselect-slider-adds-control-point-bug

This commit is contained in:
Bartłomiej Dach 2024-02-10 15:35:03 +01:00
commit 6954a185c4
No known key found for this signature in database
106 changed files with 766 additions and 338 deletions

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Objects.Drawables
{ {
if (timeOffset >= 0) if (timeOffset >= 0)
// todo: implement judgement logic // todo: implement judgement logic
ApplyResult(r => r.Type = HitResult.Perfect); ApplyResult(HitResult.Perfect);
} }
protected override void UpdateHitStateTransforms(ArmedState state) protected override void UpdateHitStateTransforms(ArmedState state)

View File

@ -9,7 +9,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -49,7 +48,12 @@ namespace osu.Game.Rulesets.Pippidon.Objects.Drawables
protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void CheckForResult(bool userTriggered, double timeOffset)
{ {
if (timeOffset >= 0) if (timeOffset >= 0)
ApplyResult(r => r.Type = IsHovered ? HitResult.Perfect : HitResult.Miss); {
if (IsHovered)
ApplyMaxResult();
else
ApplyMinResult();
}
} }
protected override double InitialLifetimeOffset => time_preempt; protected override double InitialLifetimeOffset => time_preempt;

View File

@ -3,7 +3,6 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -24,7 +23,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Objects.Drawables
{ {
if (timeOffset >= 0) if (timeOffset >= 0)
// todo: implement judgement logic // todo: implement judgement logic
ApplyResult(r => r.Type = HitResult.Perfect); ApplyMaxResult();
} }
protected override void UpdateHitStateTransforms(ArmedState state) protected override void UpdateHitStateTransforms(ArmedState state)

View File

@ -10,7 +10,6 @@ using osu.Framework.Graphics.Textures;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Pippidon.UI; using osu.Game.Rulesets.Pippidon.UI;
using osu.Game.Rulesets.Scoring;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -49,7 +48,12 @@ namespace osu.Game.Rulesets.Pippidon.Objects.Drawables
protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void CheckForResult(bool userTriggered, double timeOffset)
{ {
if (timeOffset >= 0) if (timeOffset >= 0)
ApplyResult(r => r.Type = currentLane.Value == HitObject.Lane ? HitResult.Perfect : HitResult.Miss); {
if (currentLane.Value == HitObject.Lane)
ApplyMaxResult();
else
ApplyMinResult();
}
} }
protected override void UpdateHitStateTransforms(ArmedState state) protected override void UpdateHitStateTransforms(ArmedState state)

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.131.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2024.205.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged. <!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -0,0 +1,31 @@
// 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 BenchmarkDotNet.Attributes;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Benchmarks
{
public class BenchmarkUnstableRate : BenchmarkTest
{
private List<HitEvent> events = null!;
public override void SetUp()
{
base.SetUp();
events = new List<HitEvent>();
for (int i = 0; i < 1000; i++)
events.Add(new HitEvent(RNG.NextDouble(-200.0, 200.0), RNG.NextDouble(1.0, 2.0), HitResult.Great, new HitObject(), null, null));
}
[Benchmark]
public void CalculateUnstableRate()
{
_ = events.CalculateUnstableRate();
}
}
}

View File

@ -63,7 +63,12 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
if (CheckPosition == null) return; if (CheckPosition == null) return;
if (timeOffset >= 0 && Result != null) if (timeOffset >= 0 && Result != null)
ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? r.Judgement.MaxResult : r.Judgement.MinResult); {
if (CheckPosition.Invoke(HitObject))
ApplyMaxResult();
else
ApplyMinResult();
}
} }
protected override void UpdateHitStateTransforms(ArmedState state) protected override void UpdateHitStateTransforms(ArmedState state)

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private readonly bool isForCurrentRuleset; private readonly bool isForCurrentRuleset;
private readonly double originalOverallDifficulty; private readonly double originalOverallDifficulty;
public override int Version => 20220902; public override int Version => 20230817;
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)

View File

@ -375,7 +375,7 @@ namespace osu.Game.Rulesets.Mania
/// <returns>The <see cref="PlayfieldType"/> that corresponds to <paramref name="variant"/>.</returns> /// <returns>The <see cref="PlayfieldType"/> that corresponds to <paramref name="variant"/>.</returns>
private PlayfieldType getPlayfieldType(int variant) private PlayfieldType getPlayfieldType(int variant)
{ {
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast<int>().OrderByDescending(i => i).First(v => variant >= v); return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast<int>().OrderDescending().First(v => variant >= v);
} }
protected override IEnumerable<HitResult> GetValidHitResults() protected override IEnumerable<HitResult> GetValidHitResults()

View File

@ -265,7 +265,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (Tail.AllJudged) if (Tail.AllJudged)
{ {
if (Tail.IsHit) if (Tail.IsHit)
ApplyResult(r => r.Type = r.Judgement.MaxResult); ApplyMaxResult();
else else
MissForcefully(); MissForcefully();
} }

View File

@ -25,7 +25,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{ {
if (AllJudged) return; if (AllJudged) return;
ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult); if (hit)
ApplyMaxResult();
else
ApplyMinResult();
} }
} }
} }

View File

@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// <summary> /// <summary>
/// Causes this <see cref="DrawableManiaHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>. /// Causes this <see cref="DrawableManiaHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
/// </summary> /// </summary>
public virtual void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult); public virtual void MissForcefully() => ApplyMinResult();
} }
public abstract partial class DrawableManiaHitObject<TObject> : DrawableManiaHitObject public abstract partial class DrawableManiaHitObject<TObject> : DrawableManiaHitObject

View File

@ -89,18 +89,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (!userTriggered) if (!userTriggered)
{ {
if (!HitObject.HitWindows.CanBeHit(timeOffset)) if (!HitObject.HitWindows.CanBeHit(timeOffset))
ApplyResult(r => r.Type = r.Judgement.MinResult); ApplyMinResult();
return; return;
} }
var result = HitObject.HitWindows.ResultFor(timeOffset); var result = HitObject.HitWindows.ResultFor(timeOffset);
if (result == HitResult.None) if (result == HitResult.None)
return; return;
result = GetCappedResult(result); result = GetCappedResult(result);
ApplyResult(result);
ApplyResult(r => r.Type = result);
} }
/// <summary> /// <summary>

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Scoring
} }
protected override IEnumerable<HitObject> EnumerateHitObjects(IBeatmap beatmap) protected override IEnumerable<HitObject> EnumerateHitObjects(IBeatmap beatmap)
=> base.EnumerateHitObjects(beatmap).OrderBy(ho => ho, JudgementOrderComparer.DEFAULT); => base.EnumerateHitObjects(beatmap).Order(JudgementOrderComparer.DEFAULT);
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{ {

View File

@ -1,16 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Threading;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
@ -59,10 +59,9 @@ namespace osu.Game.Rulesets.Mania.UI
// Stores the current speed adjustment active in gameplay. // Stores the current speed adjustment active in gameplay.
private readonly Track speedAdjustmentTrack = new TrackVirtual(0); private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
[Resolved] private ISkinSource currentSkin = null!;
private ISkinSource skin { get; set; }
public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)
: base(ruleset, beatmap, mods) : base(ruleset, beatmap, mods)
{ {
BarLines = new BarLineGenerator<BarLine>(Beatmap).BarLines; BarLines = new BarLineGenerator<BarLine>(Beatmap).BarLines;
@ -72,8 +71,12 @@ namespace osu.Game.Rulesets.Mania.UI
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(ISkinSource source)
{ {
currentSkin = source;
currentSkin.SourceChanged += onSkinChange;
skinChanged();
foreach (var mod in Mods.OfType<IApplicableToTrack>()) foreach (var mod in Mods.OfType<IApplicableToTrack>())
mod.ApplyToTrack(speedAdjustmentTrack); mod.ApplyToTrack(speedAdjustmentTrack);
@ -109,12 +112,28 @@ namespace osu.Game.Rulesets.Mania.UI
updateTimeRange(); updateTimeRange();
} }
private ScheduledDelegate? pendingSkinChange;
private float hitPosition;
private void onSkinChange()
{
// schedule required to avoid calls after disposed.
// note that this has the side-effect of components only performing a skin change when they are alive.
pendingSkinChange?.Cancel();
pendingSkinChange = Scheduler.Add(skinChanged);
}
private void skinChanged()
{
hitPosition = currentSkin.GetConfig<ManiaSkinConfigurationLookup, float>(
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
?? Stage.HIT_TARGET_POSITION;
pendingSkinChange = null;
}
private void updateTimeRange() private void updateTimeRange()
{ {
float hitPosition = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
?? Stage.HIT_TARGET_POSITION;
const float length_to_default_hit_position = 768 - LegacyManiaSkinConfiguration.DEFAULT_HIT_POSITION; const float length_to_default_hit_position = 768 - LegacyManiaSkinConfiguration.DEFAULT_HIT_POSITION;
float lengthToHitPosition = 768 - hitPosition; float lengthToHitPosition = 768 - hitPosition;
@ -139,10 +158,18 @@ namespace osu.Game.Rulesets.Mania.UI
protected override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant); protected override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant);
public override DrawableHitObject<ManiaHitObject> CreateDrawableRepresentation(ManiaHitObject h) => null; public override DrawableHitObject<ManiaHitObject>? CreateDrawableRepresentation(ManiaHitObject h) => null;
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay); protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay);
protected override ReplayRecorder CreateReplayRecorder(Score score) => new ManiaReplayRecorder(score); protected override ReplayRecorder CreateReplayRecorder(Score score) => new ManiaReplayRecorder(score);
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (currentSkin.IsNotNull())
currentSkin.SourceChanged -= onSkinChange;
}
} }
} }

View File

@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(130) Size = new Vector2(300)
} }
}; };
}); });
@ -85,6 +85,30 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("return user input", () => InputManager.UseParentInput = true); AddStep("return user input", () => InputManager.UseParentInput = true);
} }
[Test]
public void TestAllPoints()
{
AddStep("add points", () =>
{
float minX = object1.DrawPosition.X - object1.DrawSize.X / 2;
float maxX = object1.DrawPosition.X + object1.DrawSize.X / 2;
float minY = object1.DrawPosition.Y - object1.DrawSize.Y / 2;
float maxY = object1.DrawPosition.Y + object1.DrawSize.Y / 2;
for (int i = 0; i < 10; i++)
{
for (float x = minX; x <= maxX; x += 0.5f)
{
for (float y = minY; y <= maxY; y += 0.5f)
{
accuracyHeatmap.AddPoint(object2.Position, object1.Position, new Vector2(x, y), RNG.NextSingle(10, 500));
}
}
}
});
}
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
{ {
accuracyHeatmap.AddPoint(object2.Position, object1.Position, background.ToLocalSpace(e.ScreenSpaceMouseDownPosition), 50); accuracyHeatmap.AddPoint(object2.Position, object1.Position, background.ToLocalSpace(e.ScreenSpaceMouseDownPosition), 50);

View File

@ -136,7 +136,7 @@ namespace osu.Game.Rulesets.Osu.Tests
if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current, HitResult.Great) == ClickAction.Hit) if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current, HitResult.Great) == ClickAction.Hit)
{ {
// force success // force success
ApplyResult(r => r.Type = HitResult.Great); ApplyResult(HitResult.Great);
} }
else else
base.CheckForResult(userTriggered, timeOffset); base.CheckForResult(userTriggered, timeOffset);

View File

@ -208,7 +208,7 @@ namespace osu.Game.Rulesets.Osu.Tests
if (shouldHit && !userTriggered && timeOffset >= 0) if (shouldHit && !userTriggered && timeOffset >= 0)
{ {
// force success // force success
ApplyResult(r => r.Type = HitResult.Great); ApplyResult(HitResult.Great);
} }
else else
base.CheckForResult(userTriggered, timeOffset); base.CheckForResult(userTriggered, timeOffset);

View File

@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
// These sections will not contribute to the difficulty. // These sections will not contribute to the difficulty.
var peaks = GetCurrentStrainPeaks().Where(p => p > 0); var peaks = GetCurrentStrainPeaks().Where(p => p > 0);
List<double> strains = peaks.OrderByDescending(d => d).ToList(); List<double> strains = peaks.OrderDescending().ToList();
// We are reducing the highest strains first to account for extreme difficulty spikes // We are reducing the highest strains first to account for extreme difficulty spikes
for (int i = 0; i < Math.Min(strains.Count, ReducedSectionCount); i++) for (int i = 0; i < Math.Min(strains.Count, ReducedSectionCount); i++)
@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
// Difficulty is the weighted sum of the highest strains from every section. // Difficulty is the weighted sum of the highest strains from every section.
// We're sorting from highest to lowest strain. // We're sorting from highest to lowest strain.
foreach (double strain in strains.OrderByDescending(d => d)) foreach (double strain in strains.OrderDescending())
{ {
difficulty += strain * weight; difficulty += strain * weight;
weight *= DecayWeight; weight *= DecayWeight;

View File

@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight) };
public override bool Ranked => true;
private DrawableOsuBlinds blinds = null!; private DrawableOsuBlinds blinds = null!;

View File

@ -75,8 +75,10 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
double time = playfield.Time.Current; double time = playfield.Time.Current;
foreach (var drawable in playfield.HitObjectContainer.AliveObjects) foreach (var entry in playfield.HitObjectContainer.AliveEntries)
{ {
var drawable = entry.Value;
switch (drawable) switch (drawable)
{ {
case DrawableHitCircle circle: case DrawableHitCircle circle:

View File

@ -49,8 +49,10 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
var cursorPos = playfield.Cursor.AsNonNull().ActiveCursor.DrawPosition; var cursorPos = playfield.Cursor.AsNonNull().ActiveCursor.DrawPosition;
foreach (var drawable in playfield.HitObjectContainer.AliveObjects) foreach (var entry in playfield.HitObjectContainer.AliveEntries)
{ {
var drawable = entry.Value;
switch (drawable) switch (drawable)
{ {
case DrawableHitCircle circle: case DrawableHitCircle circle:

View File

@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods
if (!slider.HeadCircle.IsHit) if (!slider.HeadCircle.IsHit)
handleHitCircle(slider.HeadCircle); handleHitCircle(slider.HeadCircle);
requiresHold |= slider.SliderInputManager.IsMouseInFollowArea(true); requiresHold |= slider.SliderInputManager.IsMouseInFollowArea(slider.Tracking.Value);
break; break;
case DrawableSpinner spinner: case DrawableSpinner spinner:

View File

@ -48,8 +48,10 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
var cursorPos = playfield.Cursor.AsNonNull().ActiveCursor.DrawPosition; var cursorPos = playfield.Cursor.AsNonNull().ActiveCursor.DrawPosition;
foreach (var drawable in playfield.HitObjectContainer.AliveObjects) foreach (var entry in playfield.HitObjectContainer.AliveEntries)
{ {
var drawable = entry.Value;
var destination = Vector2.Clamp(2 * drawable.Position - cursorPos, Vector2.Zero, OsuPlayfield.BASE_SIZE); var destination = Vector2.Clamp(2 * drawable.Position - cursorPos, Vector2.Zero, OsuPlayfield.BASE_SIZE);
if (drawable.HitObject is Slider thisSlider) if (drawable.HitObject is Slider thisSlider)

View File

@ -155,7 +155,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (!userTriggered) if (!userTriggered)
{ {
if (!HitObject.HitWindows.CanBeHit(timeOffset)) if (!HitObject.HitWindows.CanBeHit(timeOffset))
ApplyResult(r => r.Type = r.Judgement.MinResult); ApplyMinResult();
return; return;
} }
@ -169,19 +169,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (result == HitResult.None || clickAction != ClickAction.Hit) if (result == HitResult.None || clickAction != ClickAction.Hit)
return; return;
ApplyResult(r => Vector2? hitPosition = null;
// Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss.
if (result.IsHit())
{
var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position);
hitPosition = HitObject.StackedPosition + (localMousePosition - DrawSize / 2);
}
ApplyResult<(HitResult result, Vector2? position)>((r, state) =>
{ {
var circleResult = (OsuHitCircleJudgementResult)r; var circleResult = (OsuHitCircleJudgementResult)r;
// Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss. circleResult.Type = state.result;
if (result.IsHit()) circleResult.CursorPositionAtHit = state.position;
{ }, (result, hitPosition));
var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position);
circleResult.CursorPositionAtHit = HitObject.StackedPosition + (localMousePosition - DrawSize / 2);
}
circleResult.Type = result;
});
} }
/// <summary> /// <summary>

View File

@ -100,12 +100,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// <summary> /// <summary>
/// Causes this <see cref="DrawableOsuHitObject"/> to get hit, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>. /// Causes this <see cref="DrawableOsuHitObject"/> to get hit, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
/// </summary> /// </summary>
public void HitForcefully() => ApplyResult(r => r.Type = r.Judgement.MaxResult); public void HitForcefully() => ApplyMaxResult();
/// <summary> /// <summary>
/// Causes this <see cref="DrawableOsuHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>. /// Causes this <see cref="DrawableOsuHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
/// </summary> /// </summary>
public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult); public void MissForcefully() => ApplyMinResult();
private RectangleF parentScreenSpaceRectangle => ((DrawableOsuHitObject)ParentHitObject)?.parentScreenSpaceRectangle ?? Parent!.ScreenSpaceDrawQuad.AABBFloat; private RectangleF parentScreenSpaceRectangle => ((DrawableOsuHitObject)ParentHitObject)?.parentScreenSpaceRectangle ?? Parent!.ScreenSpaceDrawQuad.AABBFloat;

View File

@ -292,10 +292,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (HitObject.ClassicSliderBehaviour) if (HitObject.ClassicSliderBehaviour)
{ {
// Classic behaviour means a slider is judged proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring. // Classic behaviour means a slider is judged proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring.
ApplyResult(r => ApplyResult(static (r, hitObject) =>
{ {
int totalTicks = NestedHitObjects.Count; int totalTicks = hitObject.NestedHitObjects.Count;
int hitTicks = NestedHitObjects.Count(h => h.IsHit); int hitTicks = hitObject.NestedHitObjects.Count(h => h.IsHit);
if (hitTicks == totalTicks) if (hitTicks == totalTicks)
r.Type = HitResult.Great; r.Type = HitResult.Great;
@ -312,7 +312,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
// If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes. // If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes.
// But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc). // But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc).
ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult); ApplyResult(static (r, hitObject) =>
{
r.Type = hitObject.NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult;
});
} }
} }

View File

@ -258,15 +258,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
foreach (var tick in ticks.Where(t => !t.Result.HasResult)) foreach (var tick in ticks.Where(t => !t.Result.HasResult))
tick.TriggerResult(false); tick.TriggerResult(false);
ApplyResult(r => ApplyResult(static (r, hitObject) =>
{ {
if (Progress >= 1) var spinner = (DrawableSpinner)hitObject;
if (spinner.Progress >= 1)
r.Type = HitResult.Great; r.Type = HitResult.Great;
else if (Progress > .9) else if (spinner.Progress > .9)
r.Type = HitResult.Ok; r.Type = HitResult.Ok;
else if (Progress > .75) else if (spinner.Progress > .75)
r.Type = HitResult.Meh; r.Type = HitResult.Meh;
else if (Time.Current >= HitObject.EndTime) else if (spinner.Time.Current >= spinner.HitObject.EndTime)
r.Type = r.Judgement.MinResult; r.Type = r.Judgement.MinResult;
}); });
} }

View File

@ -35,6 +35,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// Apply a judgement result. /// Apply a judgement result.
/// </summary> /// </summary>
/// <param name="hit">Whether this tick was reached.</param> /// <param name="hit">Whether this tick was reached.</param>
internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult); internal void TriggerResult(bool hit)
{
if (hit)
ApplyMaxResult();
else
ApplyMinResult();
}
} }
} }

View File

@ -30,7 +30,6 @@ namespace osu.Game.Rulesets.Osu.Objects
public class TailJudgement : SliderEndJudgement public class TailJudgement : SliderEndJudgement
{ {
public override HitResult MaxResult => HitResult.SliderTailHit; public override HitResult MaxResult => HitResult.SliderTailHit;
public override HitResult MinResult => HitResult.IgnoreMiss;
} }
} }
} }

View File

@ -191,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
for (int c = 0; c < points_per_dimension; c++) for (int c = 0; c < points_per_dimension; c++)
{ {
HitPointType pointType = Vector2.Distance(new Vector2(c, r), centre) <= innerRadius HitPointType pointType = Vector2.Distance(new Vector2(c + 0.5f, r + 0.5f), centre) <= innerRadius
? HitPointType.Hit ? HitPointType.Hit
: HitPointType.Miss; : HitPointType.Miss;

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
double difficulty = 0; double difficulty = 0;
double weight = 1; double weight = 1;
foreach (double strain in peaks.OrderByDescending(d => d)) foreach (double strain in peaks.OrderDescending())
{ {
difficulty += strain * weight; difficulty += strain * weight;
weight *= 0.9; weight *= 0.9;

View File

@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{ {
private const double difficulty_multiplier = 1.35; private const double difficulty_multiplier = 1.35;
public override int Version => 20220902; public override int Version => 20221107;
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)

View File

@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (timeOffset < 0) if (timeOffset < 0)
return; return;
ApplyResult(r => r.Type = r.Judgement.MaxResult); ApplyMaxResult();
} }
protected override void UpdateHitStateTransforms(ArmedState state) protected override void UpdateHitStateTransforms(ArmedState state)
@ -192,7 +192,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (!ParentHitObject.Judged) if (!ParentHitObject.Judged)
return; return;
ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); ApplyResult(static (r, hitObject) =>
{
var drumRoll = (StrongNestedHit)hitObject;
r.Type = drumRoll.ParentHitObject!.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult;
});
} }
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false; public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;

View File

@ -49,14 +49,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (!userTriggered) if (!userTriggered)
{ {
if (timeOffset > HitObject.HitWindow) if (timeOffset > HitObject.HitWindow)
ApplyResult(r => r.Type = r.Judgement.MinResult); ApplyMinResult();
return; return;
} }
if (Math.Abs(timeOffset) > HitObject.HitWindow) if (Math.Abs(timeOffset) > HitObject.HitWindow)
return; return;
ApplyResult(r => r.Type = r.Judgement.MaxResult); ApplyMaxResult();
} }
public override void OnKilled() public override void OnKilled()
@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
base.OnKilled(); base.OnKilled();
if (Time.Current > HitObject.GetEndTime() && !Judged) if (Time.Current > HitObject.GetEndTime() && !Judged)
ApplyResult(r => r.Type = r.Judgement.MinResult); ApplyMinResult();
} }
protected override void UpdateHitStateTransforms(ArmedState state) protected override void UpdateHitStateTransforms(ArmedState state)
@ -105,7 +105,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (!ParentHitObject.Judged) if (!ParentHitObject.Judged)
return; return;
ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); ApplyResult(static (r, hitObject) =>
{
var nestedHit = (StrongNestedHit)hitObject;
r.Type = nestedHit.ParentHitObject!.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult;
});
} }
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false; public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
ApplyResult(r => r.Type = r.Judgement.MaxResult); ApplyMaxResult();
} }
protected override void LoadSamples() protected override void LoadSamples()

View File

@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (!userTriggered) if (!userTriggered)
{ {
if (!HitObject.HitWindows.CanBeHit(timeOffset)) if (!HitObject.HitWindows.CanBeHit(timeOffset))
ApplyResult(r => r.Type = r.Judgement.MinResult); ApplyMinResult();
return; return;
} }
@ -108,9 +108,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
return; return;
if (!validActionPressed) if (!validActionPressed)
ApplyResult(r => r.Type = r.Judgement.MinResult); ApplyMinResult();
else else
ApplyResult(r => r.Type = result); ApplyResult(result);
} }
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
@ -209,19 +209,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (!ParentHitObject.Result.IsHit) if (!ParentHitObject.Result.IsHit)
{ {
ApplyResult(r => r.Type = r.Judgement.MinResult); ApplyMinResult();
return; return;
} }
if (!userTriggered) if (!userTriggered)
{ {
if (timeOffset - ParentHitObject.Result.TimeOffset > SECOND_HIT_WINDOW) if (timeOffset - ParentHitObject.Result.TimeOffset > SECOND_HIT_WINDOW)
ApplyResult(r => r.Type = r.Judgement.MinResult); ApplyMinResult();
return; return;
} }
if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= SECOND_HIT_WINDOW) if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= SECOND_HIT_WINDOW)
ApplyResult(r => r.Type = r.Judgement.MaxResult); ApplyMaxResult();
} }
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e)

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
// it can happen that the hit window of the nested strong hit extends past the lifetime of the parent object. // it can happen that the hit window of the nested strong hit extends past the lifetime of the parent object.
// this is a safety to prevent such cases from causing the nested hit to never be judged and as such prevent gameplay from completing. // this is a safety to prevent such cases from causing the nested hit to never be judged and as such prevent gameplay from completing.
if (!Judged && Time.Current > ParentHitObject?.HitObject.GetEndTime()) if (!Judged && Time.Current > ParentHitObject?.HitObject.GetEndTime())
ApplyResult(r => r.Type = r.Judgement.MinResult); ApplyMinResult();
} }
} }
} }

View File

@ -206,7 +206,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint);
if (numHits == HitObject.RequiredHits) if (numHits == HitObject.RequiredHits)
ApplyResult(r => r.Type = r.Judgement.MaxResult); ApplyMaxResult();
} }
else else
{ {
@ -227,7 +227,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
tick.TriggerResult(false); tick.TriggerResult(false);
} }
ApplyResult(r => r.Type = numHits == HitObject.RequiredHits ? r.Judgement.MaxResult : r.Judgement.MinResult); if (numHits == HitObject.RequiredHits)
ApplyMaxResult();
else
ApplyMinResult();
} }
} }

View File

@ -30,7 +30,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
public void TriggerResult(bool hit) public void TriggerResult(bool hit)
{ {
HitObject.StartTime = Time.Current; HitObject.StartTime = Time.Current;
ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult);
if (hit)
ApplyMaxResult();
else
ApplyMinResult();
} }
protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void CheckForResult(bool userTriggered, double timeOffset)

View File

@ -1,9 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Taiko.Configuration; using osu.Game.Rulesets.Taiko.Configuration;
@ -27,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko
{ {
new SettingsEnumDropdown<TaikoTouchControlScheme> new SettingsEnumDropdown<TaikoTouchControlScheme>
{ {
LabelText = "Touch control scheme", LabelText = RulesetSettingsStrings.TouchControlScheme,
Current = config.GetBindable<TaikoTouchControlScheme>(TaikoRulesetSetting.TouchControlScheme) Current = config.GetBindable<TaikoTouchControlScheme>(TaikoRulesetSetting.TouchControlScheme)
} }
}; };

View File

@ -216,7 +216,7 @@ namespace osu.Game.Tests.Gameplay
LifetimeStart = LIFETIME_ON_APPLY; LifetimeStart = LIFETIME_ON_APPLY;
} }
public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss); public void MissForcefully() => ApplyResult(HitResult.Miss);
protected override void UpdateHitStateTransforms(ArmedState state) protected override void UpdateHitStateTransforms(ArmedState state)
{ {

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -31,8 +29,8 @@ namespace osu.Game.Tests.Rulesets.Scoring
{ {
public partial class ScoreProcessorTest public partial class ScoreProcessorTest
{ {
private ScoreProcessor scoreProcessor; private ScoreProcessor scoreProcessor = null!;
private IBeatmap beatmap; private IBeatmap beatmap = null!;
[SetUp] [SetUp]
public void SetUp() public void SetUp()
@ -86,7 +84,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 493_652)] [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 493_652)]
[TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] [TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
[TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 326_963)] [TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 326_963)]
[TestCase(ScoringMode.Standardised, HitResult.SliderTailHit, HitResult.SliderTailHit, 326_963)] [TestCase(ScoringMode.Standardised, HitResult.SliderTailHit, HitResult.SliderTailHit, 371_627)]
[TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)] [TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)]
[TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)] [TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)]
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] [TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)]
@ -99,7 +97,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 49_365)] [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 49_365)]
[TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] [TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
[TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 32_696)] [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 32_696)]
[TestCase(ScoringMode.Classic, HitResult.SliderTailHit, HitResult.SliderTailHit, 32_696)] [TestCase(ScoringMode.Classic, HitResult.SliderTailHit, HitResult.SliderTailHit, 37_163)]
[TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 100_003)] [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 100_003)]
[TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 100_015)] [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 100_015)]
public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore) public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore)
@ -171,7 +169,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
[TestCase(HitResult.Perfect, HitResult.Miss)] [TestCase(HitResult.Perfect, HitResult.Miss)]
[TestCase(HitResult.SmallTickHit, HitResult.SmallTickMiss)] [TestCase(HitResult.SmallTickHit, HitResult.SmallTickMiss)]
[TestCase(HitResult.LargeTickHit, HitResult.LargeTickMiss)] [TestCase(HitResult.LargeTickHit, HitResult.LargeTickMiss)]
[TestCase(HitResult.SliderTailHit, HitResult.LargeTickMiss)] [TestCase(HitResult.SliderTailHit, HitResult.IgnoreMiss)]
[TestCase(HitResult.SmallBonus, HitResult.IgnoreMiss)] [TestCase(HitResult.SmallBonus, HitResult.IgnoreMiss)]
[TestCase(HitResult.LargeBonus, HitResult.IgnoreMiss)] [TestCase(HitResult.LargeBonus, HitResult.IgnoreMiss)]
public void TestMinResults(HitResult hitResult, HitResult expectedMinResult) public void TestMinResults(HitResult hitResult, HitResult expectedMinResult)
@ -476,7 +474,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
public override IEnumerable<Mod> GetModsFor(ModType type) => throw new NotImplementedException(); public override IEnumerable<Mod> GetModsFor(ModType type) => throw new NotImplementedException();
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => throw new NotImplementedException(); public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => throw new NotImplementedException();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException(); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException();

View File

@ -431,7 +431,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void CheckForResult(bool userTriggered, double timeOffset)
{ {
if (timeOffset > HitObject.Duration) if (timeOffset > HitObject.Duration)
ApplyResult(r => r.Type = r.Judgement.MaxResult); ApplyMaxResult();
} }
protected override void UpdateHitStateTransforms(ArmedState state) protected override void UpdateHitStateTransforms(ArmedState state)
@ -468,7 +468,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public override void OnKilled() public override void OnKilled()
{ {
base.OnKilled(); base.OnKilled();
ApplyResult(r => r.Type = r.Judgement.MinResult); ApplyMinResult();
} }
} }
@ -547,7 +547,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
base.CheckForResult(userTriggered, timeOffset); base.CheckForResult(userTriggered, timeOffset);
if (timeOffset >= 0) if (timeOffset >= 0)
ApplyResult(r => r.Type = r.Judgement.MaxResult); ApplyMaxResult();
} }
} }
@ -596,7 +596,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
base.CheckForResult(userTriggered, timeOffset); base.CheckForResult(userTriggered, timeOffset);
if (timeOffset >= 0) if (timeOffset >= 0)
ApplyResult(r => r.Type = r.Judgement.MaxResult); ApplyMaxResult();
} }
} }

View File

@ -64,6 +64,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("select first room", () => container.Rooms.First().TriggerClick()); AddStep("select first room", () => container.Rooms.First().TriggerClick());
AddAssert("first spotlight selected", () => checkRoomSelected(RoomManager.Rooms.First(r => r.Category.Value == RoomCategory.Spotlight))); AddAssert("first spotlight selected", () => checkRoomSelected(RoomManager.Rooms.First(r => r.Category.Value == RoomCategory.Spotlight)));
AddStep("remove last room", () => RoomManager.RemoveRoom(RoomManager.Rooms.MinBy(r => r.RoomID?.Value)));
AddAssert("first spotlight still selected", () => checkRoomSelected(RoomManager.Rooms.First(r => r.Category.Value == RoomCategory.Spotlight)));
AddStep("remove spotlight room", () => RoomManager.RemoveRoom(RoomManager.Rooms.Single(r => r.Category.Value == RoomCategory.Spotlight)));
AddAssert("selection vacated", () => checkRoomSelected(null));
} }
[Test] [Test]

View File

@ -2,14 +2,15 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
{ {
public partial class TestSceneDashboardOverlay : OsuTestScene public partial class TestSceneDashboardOverlay : OsuTestScene
{ {
protected override bool UseOnlineAPI => true;
private readonly DashboardOverlay overlay; private readonly DashboardOverlay overlay;
public TestSceneDashboardOverlay() public TestSceneDashboardOverlay()
@ -17,6 +18,30 @@ namespace osu.Game.Tests.Visual.Online
Add(overlay = new DashboardOverlay()); Add(overlay = new DashboardOverlay());
} }
[BackgroundDependencyLoader]
private void load()
{
int supportLevel = 0;
for (int i = 0; i < 1000; i++)
{
supportLevel++;
if (supportLevel > 3)
supportLevel = 0;
((DummyAPIAccess)API).Friends.Add(new APIUser
{
Username = @"peppy",
Id = 2,
Colour = "99EB47",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
IsSupporter = supportLevel > 0,
SupportLevel = supportLevel
});
}
}
[Test] [Test]
public void TestShow() public void TestShow()
{ {

View File

@ -206,6 +206,12 @@ namespace osu.Game.Tests.Visual.Online
Total = 50 Total = 50
}, },
SupportLevel = 2, SupportLevel = 2,
Location = "Somewhere",
Interests = "Rhythm games",
Occupation = "Gamer",
Twitter = "test_user",
Discord = "test_user",
Website = "https://google.com",
}; };
} }
} }

View File

@ -45,6 +45,6 @@ namespace osu.Game.Tournament.IO
Logger.Log("Changing tournament storage: " + GetFullPath(string.Empty)); Logger.Log("Changing tournament storage: " + GetFullPath(string.Empty));
} }
public IEnumerable<string> ListTournaments() => AllTournaments.GetDirectories(string.Empty).OrderBy(directory => directory, StringComparer.CurrentCultureIgnoreCase); public IEnumerable<string> ListTournaments() => AllTournaments.GetDirectories(string.Empty).Order(StringComparer.CurrentCultureIgnoreCase);
} }
} }

View File

@ -266,8 +266,8 @@ namespace osu.Game.Beatmaps
if (!base.CanReuseExisting(existing, import)) if (!base.CanReuseExisting(existing, import))
return false; return false;
var existingIds = existing.Beatmaps.Select(b => b.OnlineID).OrderBy(i => i); var existingIds = existing.Beatmaps.Select(b => b.OnlineID).Order();
var importIds = import.Beatmaps.Select(b => b.OnlineID).OrderBy(i => i); var importIds = import.Beatmaps.Select(b => b.OnlineID).Order();
// force re-import if we are not in a sane state. // force re-import if we are not in a sane state.
return existing.OnlineID == import.OnlineID && existingIds.SequenceEqual(importIds); return existing.OnlineID == import.OnlineID && existingIds.SequenceEqual(importIds);

View File

@ -74,7 +74,7 @@ namespace osu.Game.Collections
} }
else else
{ {
foreach (int i in changes.DeletedIndices.OrderByDescending(i => i)) foreach (int i in changes.DeletedIndices.OrderDescending())
filters.RemoveAt(i + 1); filters.RemoveAt(i + 1);
foreach (int i in changes.InsertedIndices) foreach (int i in changes.InsertedIndices)

View File

@ -279,7 +279,7 @@ namespace osu.Game.Database
// note that this should really be checking filesizes on disk (of existing files) for some degree of sanity. // note that this should really be checking filesizes on disk (of existing files) for some degree of sanity.
// or alternatively doing a faster hash check. either of these require database changes and reprocessing of existing files. // or alternatively doing a faster hash check. either of these require database changes and reprocessing of existing files.
if (CanSkipImport(existing, item) && if (CanSkipImport(existing, item) &&
getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f)) && getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).Order()) &&
checkAllFilesExist(existing)) checkAllFilesExist(existing))
{ {
LogForModel(item, @$"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) skipping import."); LogForModel(item, @$"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) skipping import.");
@ -437,7 +437,7 @@ namespace osu.Game.Database
{ {
MemoryStream hashable = new MemoryStream(); MemoryStream hashable = new MemoryStream();
foreach (string? file in reader.Filenames.Where(f => HashableFileTypes.Any(ext => f.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f)) foreach (string? file in reader.Filenames.Where(f => HashableFileTypes.Any(ext => f.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).Order())
{ {
using (Stream s = reader.GetStream(file)) using (Stream s = reader.GetStream(file))
s.CopyTo(hashable); s.CopyTo(hashable);

View File

@ -64,6 +64,21 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString AudioOffset => new TranslatableString(getKey(@"audio_offset"), @"Audio offset"); public static LocalisableString AudioOffset => new TranslatableString(getKey(@"audio_offset"), @"Audio offset");
/// <summary>
/// "Play a few beatmaps to receive a suggested offset!"
/// </summary>
public static LocalisableString SuggestedOffsetNote => new TranslatableString(getKey(@"suggested_offset_note"), @"Play a few beatmaps to receive a suggested offset!");
/// <summary>
/// "Based on the last {0} play(s), the suggested offset is {1} ms."
/// </summary>
public static LocalisableString SuggestedOffsetValueReceived(int plays, LocalisableString value) => new TranslatableString(getKey(@"suggested_offset_value_received"), @"Based on the last {0} play(s), the suggested offset is {1} ms.", plays, value);
/// <summary>
/// "Apply suggested offset"
/// </summary>
public static LocalisableString ApplySuggestedOffset => new TranslatableString(getKey(@"apply_suggested_offset"), @"Apply suggested offset");
/// <summary> /// <summary>
/// "Offset wizard" /// "Offset wizard"
/// </summary> /// </summary>

View File

@ -29,6 +29,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString ImportFiles => new TranslatableString(getKey(@"import_files"), @"Import files"); public static LocalisableString ImportFiles => new TranslatableString(getKey(@"import_files"), @"Import files");
/// <summary>
/// "Run latency certifier"
/// </summary>
public static LocalisableString RunLatencyCertifier => new TranslatableString(getKey(@"run_latency_certifier"), @"Run latency certifier");
/// <summary> /// <summary>
/// "Memory" /// "Memory"
/// </summary> /// </summary>

View File

@ -51,6 +51,31 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString Items(int arg0) => new TranslatableString(getKey(@"items"), @"{0} item(s)", arg0); public static LocalisableString Items(int arg0) => new TranslatableString(getKey(@"items"), @"{0} item(s)", arg0);
/// <summary>
/// "Data migration will use &quot;hard links&quot;. No extra disk space will be used, and you can delete either data folder at any point without affecting the other installation."
/// </summary>
public static LocalisableString DataMigrationNoExtraSpace => new TranslatableString(getKey(@"data_migration_no_extra_space"), @"Data migration will use ""hard links"". No extra disk space will be used, and you can delete either data folder at any point without affecting the other installation.");
/// <summary>
/// "Learn more about how &quot;hard links&quot; work"
/// </summary>
public static LocalisableString LearnAboutHardLinks => new TranslatableString(getKey(@"learn_about_hard_links"), @"Learn more about how ""hard links"" work");
/// <summary>
/// "Lightweight linking of files is not supported on your operating system yet, so a copy of all files will be made during import."
/// </summary>
public static LocalisableString LightweightLinkingNotSupported => new TranslatableString(getKey(@"lightweight_linking_not_supported"), @"Lightweight linking of files is not supported on your operating system yet, so a copy of all files will be made during import.");
/// <summary>
/// "A second copy of all files will be made during import. To avoid this, please make sure the lazer data folder is on the same drive as your previous osu! install (and the file system is NTFS)."
/// </summary>
public static LocalisableString SecondCopyWillBeMadeWindows => new TranslatableString(getKey(@"second_copy_will_be_made_windows"), @"A second copy of all files will be made during import. To avoid this, please make sure the lazer data folder is on the same drive as your previous osu! install (and the file system is NTFS).");
/// <summary>
/// "A second copy of all files will be made during import. To avoid this, please make sure the lazer data folder is on the same drive as your previous osu! install (and the file system supports hard links)."
/// </summary>
public static LocalisableString SecondCopyWillBeMadeOtherPlatforms => new TranslatableString(getKey(@"second_copy_will_be_made_other_platforms"), @"A second copy of all files will be made during import. To avoid this, please make sure the lazer data folder is on the same drive as your previous osu! install (and the file system supports hard links).");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -159,6 +159,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString MinimiseOnFocusLoss => new TranslatableString(getKey(@"minimise_on_focus_loss"), @"Minimise osu! when switching to another app"); public static LocalisableString MinimiseOnFocusLoss => new TranslatableString(getKey(@"minimise_on_focus_loss"), @"Minimise osu! when switching to another app");
/// <summary>
/// "Shrink game to avoid cameras and notches"
/// </summary>
public static LocalisableString ShrinkGameToSafeArea => new TranslatableString(getKey(@"shrink_game_to_safe_area"), @"Shrink game to avoid cameras and notches");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -113,6 +113,28 @@ Please try changing your audio device to a working setting.");
/// </summary> /// </summary>
public static LocalisableString MismatchingBeatmapForReplay => new TranslatableString(getKey(@"mismatching_beatmap_for_replay"), @"Your local copy of the beatmap for this replay appears to be different than expected. You may need to update or re-download it."); public static LocalisableString MismatchingBeatmapForReplay => new TranslatableString(getKey(@"mismatching_beatmap_for_replay"), @"Your local copy of the beatmap for this replay appears to be different than expected. You may need to update or re-download it.");
/// <summary>
/// "You are now running osu! {version}.
/// Click to see what's new!"
/// </summary>
public static LocalisableString GameVersionAfterUpdate(string version) => new TranslatableString(getKey(@"game_version_after_update"), @"You are now running osu! {0}.
Click to see what's new!", version);
/// <summary>
/// "Update ready to install. Click to restart!"
/// </summary>
public static LocalisableString UpdateReadyToInstall => new TranslatableString(getKey(@"update_ready_to_install"), @"Update ready to install. Click to restart!");
/// <summary>
/// "Downloading update..."
/// </summary>
public static LocalisableString DownloadingUpdate => new TranslatableString(getKey(@"downloading_update"), @"Downloading update...");
/// <summary>
/// "Installing update..."
/// </summary>
public static LocalisableString InstallingUpdate => new TranslatableString(getKey(@"installing_update"), @"Installing update...");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -84,6 +84,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString ScrollSpeedTooltip(int scrollTime, int scrollSpeed) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1})", scrollTime, scrollSpeed); public static LocalisableString ScrollSpeedTooltip(int scrollTime, int scrollSpeed) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1})", scrollTime, scrollSpeed);
/// <summary>
/// "Touch control scheme"
/// </summary>
public static LocalisableString TouchControlScheme => new TranslatableString(getKey(@"touch_control_scheme"), @"Touch control scheme");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -203,6 +203,7 @@ namespace osu.Game.Online.API.Requests.Responses
Ruleset = new RulesetInfo { OnlineID = RulesetID }, Ruleset = new RulesetInfo { OnlineID = RulesetID },
Passed = Passed, Passed = Passed,
TotalScore = TotalScore, TotalScore = TotalScore,
LegacyTotalScore = LegacyTotalScore,
Accuracy = Accuracy, Accuracy = Accuracy,
MaxCombo = MaxCombo, MaxCombo = MaxCombo,
Rank = Rank, Rank = Rank,

View File

@ -23,9 +23,12 @@ namespace osu.Game.Online.Multiplayer
Debug.Assert(exception != null); Debug.Assert(exception != null);
string message = exception.GetHubExceptionMessage() ?? exception.Message; if (exception.GetHubExceptionMessage() is string message)
// Hub exceptions generally contain something we can show the user directly.
Logger.Log(message, level: LogLevel.Important);
else
Logger.Error(exception, $"Unobserved exception occurred via {nameof(FireAndForget)} call: {exception.Message}");
Logger.Log(message, level: LogLevel.Important);
onError?.Invoke(exception); onError?.Invoke(exception);
} }
else else

View File

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Utils;
using osu.Game.Online.API; using osu.Game.Online.API;
namespace osu.Game.Online namespace osu.Game.Online
@ -31,6 +32,12 @@ namespace osu.Game.Online
private CancellationTokenSource connectCancelSource = new CancellationTokenSource(); private CancellationTokenSource connectCancelSource = new CancellationTokenSource();
private bool started; private bool started;
/// <summary>
/// How much to delay before attempting to connect again, in milliseconds.
/// Subject to exponential back-off.
/// </summary>
private int retryDelay = 3000;
/// <summary> /// <summary>
/// Constructs a new <see cref="PersistentEndpointClientConnector"/>. /// Constructs a new <see cref="PersistentEndpointClientConnector"/>.
/// </summary> /// </summary>
@ -78,6 +85,8 @@ namespace osu.Game.Online
private async Task connect() private async Task connect()
{ {
cancelExistingConnect(); cancelExistingConnect();
// reset retry delay to default.
retryDelay = 3000;
if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false)) if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck."); throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck.");
@ -134,8 +143,15 @@ namespace osu.Game.Online
/// </summary> /// </summary>
private async Task handleErrorAndDelay(Exception exception, CancellationToken cancellationToken) private async Task handleErrorAndDelay(Exception exception, CancellationToken cancellationToken)
{ {
Logger.Log($"{ClientName} connect attempt failed: {exception.Message}", LoggingTarget.Network); // random stagger factor to avoid mass incidental synchronisation
await Task.Delay(5000, cancellationToken).ConfigureAwait(false); // compare: https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/Online/BanchoClient.cs#L331
int thisDelay = (int)(retryDelay * RNG.NextDouble(0.75, 1.25));
// exponential backoff with upper limit
// compare: https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/Online/BanchoClient.cs#L539
retryDelay = Math.Min(120000, (int)(retryDelay * 1.5));
Logger.Log($"{ClientName} connect attempt failed: {exception.Message}. Next attempt in {thisDelay / 1000:N0} seconds.", LoggingTarget.Network);
await Task.Delay(thisDelay, cancellationToken).ConfigureAwait(false);
} }
/// <summary> /// <summary>

View File

@ -1,8 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Globalization;
using System.Net.Http; using System.Net.Http;
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
using osu.Game.Beatmaps;
using osu.Game.Online.API; using osu.Game.Online.API;
namespace osu.Game.Online.Rooms namespace osu.Game.Online.Rooms
@ -11,12 +13,16 @@ namespace osu.Game.Online.Rooms
{ {
private readonly long roomId; private readonly long roomId;
private readonly long playlistItemId; private readonly long playlistItemId;
private readonly BeatmapInfo beatmapInfo;
private readonly int rulesetId;
private readonly string versionHash; private readonly string versionHash;
public CreateRoomScoreRequest(long roomId, long playlistItemId, string versionHash) public CreateRoomScoreRequest(long roomId, long playlistItemId, BeatmapInfo beatmapInfo, int rulesetId, string versionHash)
{ {
this.roomId = roomId; this.roomId = roomId;
this.playlistItemId = playlistItemId; this.playlistItemId = playlistItemId;
this.beatmapInfo = beatmapInfo;
this.rulesetId = rulesetId;
this.versionHash = versionHash; this.versionHash = versionHash;
} }
@ -25,6 +31,8 @@ namespace osu.Game.Online.Rooms
var req = base.CreateWebRequest(); var req = base.CreateWebRequest();
req.Method = HttpMethod.Post; req.Method = HttpMethod.Post;
req.AddParameter("version_hash", versionHash); req.AddParameter("version_hash", versionHash);
req.AddParameter("beatmap_hash", beatmapInfo.MD5Hash);
req.AddParameter("ruleset_id", rulesetId.ToString(CultureInfo.InvariantCulture));
return req; return req;
} }

View File

@ -127,18 +127,17 @@ namespace osu.Game.Overlays.FirstRunSetup
if (available) if (available)
{ {
copyInformation.Text = copyInformation.Text = FirstRunOverlayImportFromStableScreenStrings.DataMigrationNoExtraSpace;
"Data migration will use \"hard links\". No extra disk space will be used, and you can delete either data folder at any point without affecting the other installation. "; copyInformation.AddLink(FirstRunOverlayImportFromStableScreenStrings.LearnAboutHardLinks, LinkAction.OpenWiki, @"Client/Release_stream/Lazer/File_storage#via-hard-links");
copyInformation.AddLink("Learn more about how \"hard links\" work", LinkAction.OpenWiki, @"Client/Release_stream/Lazer/File_storage#via-hard-links");
} }
else if (!RuntimeInfo.IsDesktop) else if (!RuntimeInfo.IsDesktop)
copyInformation.Text = "Lightweight linking of files is not supported on your operating system yet, so a copy of all files will be made during import."; copyInformation.Text = FirstRunOverlayImportFromStableScreenStrings.LightweightLinkingNotSupported;
else else
{ {
copyInformation.Text = RuntimeInfo.OS == RuntimeInfo.Platform.Windows copyInformation.Text = RuntimeInfo.OS == RuntimeInfo.Platform.Windows
? "A second copy of all files will be made during import. To avoid this, please make sure the lazer data folder is on the same drive as your previous osu! install (and the file system is NTFS). " ? FirstRunOverlayImportFromStableScreenStrings.SecondCopyWillBeMadeWindows
: "A second copy of all files will be made during import. To avoid this, please make sure the lazer data folder is on the same drive as your previous osu! install (and the file system supports hard links). "; : FirstRunOverlayImportFromStableScreenStrings.SecondCopyWillBeMadeOtherPlatforms;
copyInformation.AddText(@" "); // just to ensure correct spacing
copyInformation.AddLink(GeneralSettingsStrings.ChangeFolderLocation, () => copyInformation.AddLink(GeneralSettingsStrings.ChangeFolderLocation, () =>
{ {
game?.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen())); game?.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen()));

View File

@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Mods
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Direction = FillDirection.Horizontal, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Spacing = new Vector2(7), Spacing = new Vector2(7),
Children = new Drawable[] Children = new Drawable[]

View File

@ -122,7 +122,7 @@ namespace osu.Game.Overlays.Music
foreach (int i in changes.InsertedIndices) foreach (int i in changes.InsertedIndices)
beatmapSets.Insert(i, sender[i].ToLive(realm)); beatmapSets.Insert(i, sender[i].ToLive(realm));
foreach (int i in changes.DeletedIndices.OrderByDescending(i => i)) foreach (int i in changes.DeletedIndices.OrderDescending())
beatmapSets.RemoveAt(i); beatmapSets.RemoveAt(i);
} }

View File

@ -144,8 +144,8 @@ namespace osu.Game.Overlays.Profile.Header
bool anyInfoAdded = false; bool anyInfoAdded = false;
anyInfoAdded |= tryAddInfo(FontAwesome.Solid.MapMarker, user.Location); anyInfoAdded |= tryAddInfo(FontAwesome.Solid.MapMarkerAlt, user.Location);
anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Heart, user.Interests); anyInfoAdded |= tryAddInfo(FontAwesome.Regular.Heart, user.Interests);
anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Suitcase, user.Occupation); anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Suitcase, user.Occupation);
if (anyInfoAdded) if (anyInfoAdded)
@ -171,7 +171,7 @@ namespace osu.Game.Overlays.Profile.Header
bottomLinkContainer.AddIcon(icon, text => bottomLinkContainer.AddIcon(icon, text =>
{ {
text.Font = text.Font.With(size: 10); text.Font = text.Font.With(icon.Family, 10, icon.Weight);
text.Colour = iconColour; text.Colour = iconColour;
}); });

View File

@ -1,13 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Users; using osu.Game.Users;
@ -19,8 +17,8 @@ namespace osu.Game.Overlays.Rankings
public Bindable<CountryCode> Country => countryFilter.Current; public Bindable<CountryCode> Country => countryFilter.Current;
private OverlayRulesetSelector rulesetSelector; private OverlayRulesetSelector rulesetSelector = null!;
private CountryFilter countryFilter; private CountryFilter countryFilter = null!;
protected override OverlayTitle CreateTitle() => new RankingsTitle(); protected override OverlayTitle CreateTitle() => new RankingsTitle();
@ -39,5 +37,30 @@ namespace osu.Game.Overlays.Rankings
Icon = OsuIcon.Ranking; Icon = OsuIcon.Ranking;
} }
} }
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(scope =>
{
rulesetSelector.FadeTo(showRulesetSelector(scope.NewValue) ? 1 : 0, 200, Easing.OutQuint);
}, true);
bool showRulesetSelector(RankingsScope scope)
{
switch (scope)
{
case RankingsScope.Performance:
case RankingsScope.Spotlights:
case RankingsScope.Score:
case RankingsScope.Country:
return true;
default:
return false;
}
}
}
} }
} }

View File

@ -8,6 +8,7 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -91,7 +92,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
applySuggestion = new RoundedButton applySuggestion = new RoundedButton
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Text = "Apply suggested offset", Text = AudioSettingsStrings.ApplySuggestedOffset,
Action = () => Action = () =>
{ {
if (SuggestedOffset.Value.HasValue) if (SuggestedOffset.Value.HasValue)
@ -155,8 +156,8 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
private void updateHintText() private void updateHintText()
{ {
hintText.Text = SuggestedOffset.Value == null hintText.Text = SuggestedOffset.Value == null
? @"Play a few beatmaps to receive a suggested offset!" ? AudioSettingsStrings.SuggestedOffsetNote
: $@"Based on the last {averageHitErrorHistory.Count} play(s), the suggested offset is {SuggestedOffset.Value:N0} ms."; : AudioSettingsStrings.SuggestedOffsetValueReceived(averageHitErrorHistory.Count, SuggestedOffset.Value.ToLocalisableString(@"N0"));
applySuggestion.Enabled.Value = SuggestedOffset.Value != null; applySuggestion.Enabled.Value = SuggestedOffset.Value != null;
} }

View File

@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings
}, },
new SettingsButton new SettingsButton
{ {
Text = @"Run latency certifier", Text = DebugSettingsStrings.RunLatencyCertifier,
Action = () => performer?.PerformFromScreen(menu => menu.Push(new LatencyCertifierScreen())) Action = () => performer?.PerformFromScreen(menu => menu.Push(new LatencyCertifierScreen()))
} }
}; };

View File

@ -115,7 +115,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
}, },
safeAreaConsiderationsCheckbox = new SettingsCheckbox safeAreaConsiderationsCheckbox = new SettingsCheckbox
{ {
LabelText = "Shrink game to avoid cameras and notches", LabelText = GraphicsSettingsStrings.ShrinkGameToSafeArea,
Current = osuConfig.GetBindable<bool>(OsuSetting.SafeAreaConsiderations), Current = osuConfig.GetBindable<bool>(OsuSetting.SafeAreaConsiderations),
}, },
new SettingsSlider<float, UIScaleSlider> new SettingsSlider<float, UIScaleSlider>

View File

@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
{ {
LabelText = GraphicsSettingsStrings.Renderer, LabelText = GraphicsSettingsStrings.Renderer,
Current = renderer, Current = renderer,
Items = host.GetPreferredRenderersForCurrentPlatform().OrderBy(t => t).Where(t => t != RendererType.Vulkan), Items = host.GetPreferredRenderersForCurrentPlatform().Order().Where(t => t != RendererType.Vulkan),
Keywords = new[] { @"compatibility", @"directx" }, Keywords = new[] { @"compatibility", @"directx" },
}, },
// TODO: this needs to be a custom dropdown at some point // TODO: this needs to be a custom dropdown at some point

View File

@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills
// Difficulty is the weighted sum of the highest strains from every section. // Difficulty is the weighted sum of the highest strains from every section.
// We're sorting from highest to lowest strain. // We're sorting from highest to lowest strain.
foreach (double strain in peaks.OrderByDescending(d => d)) foreach (double strain in peaks.OrderDescending())
{ {
difficulty += strain * weight; difficulty += strain * weight;
weight *= DecayWeight; weight *= DecayWeight;

View File

@ -31,8 +31,8 @@ namespace osu.Game.Rulesets.Edit.Checks
public IEnumerable<Issue> Run(BeatmapVerifierContext context) public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{ {
var startTimes = context.Beatmap.HitObjects.Select(ho => ho.StartTime).OrderBy(x => x).ToList(); var startTimes = context.Beatmap.HitObjects.Select(ho => ho.StartTime).Order().ToList();
var endTimes = context.Beatmap.HitObjects.Select(ho => ho.GetEndTime()).OrderBy(x => x).ToList(); var endTimes = context.Beatmap.HitObjects.Select(ho => ho.GetEndTime()).Order().ToList();
foreach (var breakPeriod in context.Beatmap.Breaks) foreach (var breakPeriod in context.Beatmap.Breaks)
{ {

View File

@ -73,9 +73,11 @@ namespace osu.Game.Rulesets.Judgements
return HitResult.SmallTickMiss; return HitResult.SmallTickMiss;
case HitResult.LargeTickHit: case HitResult.LargeTickHit:
case HitResult.SliderTailHit:
return HitResult.LargeTickMiss; return HitResult.LargeTickMiss;
case HitResult.SliderTailHit:
return HitResult.IgnoreMiss;
default: default:
return HitResult.Miss; return HitResult.Miss;
} }

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Judgements
/// <summary> /// <summary>
/// The time at which this <see cref="JudgementResult"/> occurred. /// The time at which this <see cref="JudgementResult"/> occurred.
/// Populated when this <see cref="JudgementResult"/> is applied via <see cref="DrawableHitObject.ApplyResult"/>. /// Populated when this <see cref="JudgementResult"/> is applied via <see cref="DrawableHitObject.ApplyResult{T}"/>.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This is used instead of <see cref="TimeAbsolute"/> to check whether this <see cref="JudgementResult"/> should be reverted. /// This is used instead of <see cref="TimeAbsolute"/> to check whether this <see cref="JudgementResult"/> should be reverted.

View File

@ -31,6 +31,8 @@ namespace osu.Game.Rulesets.Mods
public override bool RequiresConfiguration => false; public override bool RequiresConfiguration => false;
public override bool Ranked => true;
public override string SettingDescription => base.SettingDescription.Replace(MinimumAccuracy.ToString(), MinimumAccuracy.Value.ToString("##%", NumberFormatInfo.InvariantInfo)); public override string SettingDescription => base.SettingDescription.Replace(MinimumAccuracy.ToString(), MinimumAccuracy.Value.ToString("##%", NumberFormatInfo.InvariantInfo));
[SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(SettingsPercentageSlider<double>))] [SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(SettingsPercentageSlider<double>))]

View File

@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.Mods
public void ApplyToBeatmap(IBeatmap beatmap) public void ApplyToBeatmap(IBeatmap beatmap)
{ {
var hitObjects = getAllApplicableHitObjects(beatmap.HitObjects).ToList(); var hitObjects = getAllApplicableHitObjects(beatmap.HitObjects).ToList();
var endTimes = hitObjects.Select(x => x.GetEndTime()).OrderBy(x => x).Distinct().ToList(); var endTimes = hitObjects.Select(x => x.GetEndTime()).Order().Distinct().ToList();
foreach (HitObject hitObject in hitObjects) foreach (HitObject hitObject in hitObjects)
{ {

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => OsuIcon.ModDoubleTime; public override IconUsage? Icon => OsuIcon.ModDoubleTime;
public override ModType Type => ModType.DifficultyIncrease; public override ModType Type => ModType.DifficultyIncrease;
public override LocalisableString Description => "Zoooooooooom..."; public override LocalisableString Description => "Zoooooooooom...";
public override bool Ranked => UsesDefaultConfiguration; public override bool Ranked => SpeedChange.IsDefault;
[SettingSource("Speed increase", "The actual increase to apply", SettingControlType = typeof(MultiplierSettingsSlider))] [SettingSource("Speed increase", "The actual increase to apply", SettingControlType = typeof(MultiplierSettingsSlider))]
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(1.5) public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(1.5)

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => OsuIcon.ModHalftime; public override IconUsage? Icon => OsuIcon.ModHalftime;
public override ModType Type => ModType.DifficultyReduction; public override ModType Type => ModType.DifficultyReduction;
public override LocalisableString Description => "Less zoom..."; public override LocalisableString Description => "Less zoom...";
public override bool Ranked => UsesDefaultConfiguration; public override bool Ranked => SpeedChange.IsDefault;
[SettingSource("Speed decrease", "The actual decrease to apply", SettingControlType = typeof(MultiplierSettingsSlider))] [SettingSource("Speed decrease", "The actual decrease to apply", SettingControlType = typeof(MultiplierSettingsSlider))]
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(0.75) public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(0.75)

View File

@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override IconUsage? Icon => FontAwesome.Solid.EyeSlash; public override IconUsage? Icon => FontAwesome.Solid.EyeSlash;
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override bool Ranked => true;
/// <summary> /// <summary>
/// Slightly higher than the cutoff for <see cref="Drawable.IsPresent"/>. /// Slightly higher than the cutoff for <see cref="Drawable.IsPresent"/>.

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.DifficultyIncrease; public override ModType Type => ModType.DifficultyIncrease;
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override LocalisableString Description => "SS or quit."; public override LocalisableString Description => "SS or quit.";
public override bool Ranked => UsesDefaultConfiguration; public override bool Ranked => true;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModSuddenDeath), typeof(ModAccuracyChallenge) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModSuddenDeath), typeof(ModAccuracyChallenge) }).ToArray();

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.DifficultyIncrease; public override ModType Type => ModType.DifficultyIncrease;
public override LocalisableString Description => "Miss and fail."; public override LocalisableString Description => "Miss and fail.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override bool Ranked => UsesDefaultConfiguration; public override bool Ranked => true;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModPerfect)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModPerfect)).ToArray();

View File

@ -683,17 +683,31 @@ namespace osu.Game.Rulesets.Objects.Drawables
UpdateResult(false); UpdateResult(false);
} }
protected void ApplyMaxResult() => ApplyResult((r, _) => r.Type = r.Judgement.MaxResult);
protected void ApplyMinResult() => ApplyResult((r, _) => r.Type = r.Judgement.MinResult);
protected void ApplyResult(HitResult type) => ApplyResult(static (result, state) => result.Type = state, type);
[Obsolete("Use overload with state, preferrably with static delegates to avoid allocation overhead.")] // Can be removed 2024-07-26
protected void ApplyResult(Action<JudgementResult> application) => ApplyResult((r, _) => application(r), this);
protected void ApplyResult(Action<JudgementResult, DrawableHitObject> application) => ApplyResult(application, this);
/// <summary> /// <summary>
/// Applies the <see cref="Result"/> of this <see cref="DrawableHitObject"/>, notifying responders such as /// Applies the <see cref="Result"/> of this <see cref="DrawableHitObject"/>, notifying responders such as
/// the <see cref="ScoreProcessor"/> of the <see cref="JudgementResult"/>. /// the <see cref="ScoreProcessor"/> of the <see cref="JudgementResult"/>.
/// </summary> /// </summary>
/// <param name="application">The callback that applies changes to the <see cref="JudgementResult"/>.</param> /// <param name="application">The callback that applies changes to the <see cref="JudgementResult"/>. Using a `static` delegate is recommended to avoid allocation overhead.</param>
protected void ApplyResult(Action<JudgementResult> application) /// <param name="state">
/// Use this parameter to pass any data that <paramref name="application"/> requires
/// to apply a result, so that it can remain a `static` delegate and thus not allocate.
/// </param>
protected void ApplyResult<T>(Action<JudgementResult, T> application, T state)
{ {
if (Result.HasResult) if (Result.HasResult)
throw new InvalidOperationException("Cannot apply result on a hitobject that already has a result."); throw new InvalidOperationException("Cannot apply result on a hitobject that already has a result.");
application?.Invoke(Result); application?.Invoke(Result, state);
if (!Result.HasResult) if (!Result.HasResult)
throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}."); throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}.");
@ -738,7 +752,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// Checks if a scoring result has occurred for this <see cref="DrawableHitObject"/>. /// Checks if a scoring result has occurred for this <see cref="DrawableHitObject"/>.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// If a scoring result has occurred, this method must invoke <see cref="ApplyResult"/> to update the result and notify responders. /// If a scoring result has occurred, this method must invoke <see cref="ApplyResult{T}"/> to update the result and notify responders.
/// </remarks> /// </remarks>
/// <param name="userTriggered">Whether the user triggered this check.</param> /// <param name="userTriggered">Whether the user triggered this check.</param>
/// <param name="timeOffset">The offset from the end time of the <see cref="HitObject"/> at which this check occurred. /// <param name="timeOffset">The offset from the end time of the <see cref="HitObject"/> at which this check occurred.

View File

@ -4,9 +4,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.ListExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Performance;
using osu.Framework.Lists;
namespace osu.Game.Rulesets.Objects.Pooling namespace osu.Game.Rulesets.Objects.Pooling
{ {
@ -35,7 +37,7 @@ namespace osu.Game.Rulesets.Objects.Pooling
/// <remarks> /// <remarks>
/// The enumeration order is undefined. /// The enumeration order is undefined.
/// </remarks> /// </remarks>
public IEnumerable<(TEntry Entry, TDrawable Drawable)> AliveEntries => aliveDrawableMap.Select(x => (x.Key, x.Value)); public readonly SlimReadOnlyDictionaryWrapper<TEntry, TDrawable> AliveEntries;
/// <summary> /// <summary>
/// Whether to remove an entry when clock goes backward and crossed its <see cref="LifetimeEntry.LifetimeStart"/>. /// Whether to remove an entry when clock goes backward and crossed its <see cref="LifetimeEntry.LifetimeStart"/>.
@ -63,6 +65,8 @@ namespace osu.Game.Rulesets.Objects.Pooling
lifetimeManager.EntryBecameAlive += entryBecameAlive; lifetimeManager.EntryBecameAlive += entryBecameAlive;
lifetimeManager.EntryBecameDead += entryBecameDead; lifetimeManager.EntryBecameDead += entryBecameDead;
lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary; lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary;
AliveEntries = aliveDrawableMap.AsSlimReadOnly();
} }
/// <summary> /// <summary>

View File

@ -107,7 +107,7 @@ namespace osu.Game.Rulesets
} }
} }
availableRulesets.AddRange(detachedRulesets.OrderBy(r => r)); availableRulesets.AddRange(detachedRulesets.Order());
}); });
} }

View File

@ -13,6 +13,9 @@ namespace osu.Game.Rulesets.Scoring
/// <summary> /// <summary>
/// Calculates the "unstable rate" for a sequence of <see cref="HitEvent"/>s. /// Calculates the "unstable rate" for a sequence of <see cref="HitEvent"/>s.
/// </summary> /// </summary>
/// <remarks>
/// Uses <a href="https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm">Welford's online algorithm</a>.
/// </remarks>
/// <returns> /// <returns>
/// A non-null <see langword="double"/> value if unstable rate could be calculated, /// A non-null <see langword="double"/> value if unstable rate could be calculated,
/// and <see langword="null"/> if unstable rate cannot be calculated due to <paramref name="hitEvents"/> being empty. /// and <see langword="null"/> if unstable rate cannot be calculated due to <paramref name="hitEvents"/> being empty.
@ -21,9 +24,28 @@ namespace osu.Game.Rulesets.Scoring
{ {
Debug.Assert(hitEvents.All(ev => ev.GameplayRate != null)); Debug.Assert(hitEvents.All(ev => ev.GameplayRate != null));
// Division by gameplay rate is to account for TimeOffset scaling with gameplay rate. int count = 0;
double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset / ev.GameplayRate!.Value).ToArray(); double mean = 0;
return 10 * standardDeviation(timeOffsets); double sumOfSquares = 0;
foreach (var e in hitEvents)
{
if (!affectsUnstableRate(e))
continue;
count++;
// Division by gameplay rate is to account for TimeOffset scaling with gameplay rate.
double currentValue = e.TimeOffset / e.GameplayRate!.Value;
double nextMean = mean + (currentValue - mean) / count;
sumOfSquares += (currentValue - mean) * (currentValue - nextMean);
mean = nextMean;
}
if (count == 0)
return null;
return 10.0 * Math.Sqrt(sumOfSquares / count);
} }
/// <summary> /// <summary>
@ -44,15 +66,5 @@ namespace osu.Game.Rulesets.Scoring
} }
private static bool affectsUnstableRate(HitEvent e) => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit(); private static bool affectsUnstableRate(HitEvent e) => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit();
private static double? standardDeviation(double[] timeOffsets)
{
if (timeOffsets.Length == 0)
return null;
double mean = timeOffsets.Average();
double squares = timeOffsets.Select(offset => Math.Pow(offset - mean, 2)).Sum();
return Math.Sqrt(squares / timeOffsets.Length);
}
} }
} }

View File

@ -138,7 +138,8 @@ namespace osu.Game.Rulesets.Scoring
ComboBreak, ComboBreak,
/// <summary> /// <summary>
/// A special judgement similar to <see cref="LargeTickHit"/> that's used to increase the valuation of the final tick of a slider. /// A special tick judgement to increase the valuation of the final tick of a slider.
/// The default minimum result is <see cref="IgnoreMiss"/>, but may be overridden to <see cref="LargeTickMiss"/>.
/// </summary> /// </summary>
[EnumMember(Value = "slider_tail_hit")] [EnumMember(Value = "slider_tail_hit")]
[Order(8)] [Order(8)]

View File

@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.UI
// If required, we can make this lookup more efficient by adding support to get next-future-entry in LifetimeEntryManager. // If required, we can make this lookup more efficient by adding support to get next-future-entry in LifetimeEntryManager.
var candidate = var candidate =
// Use alive entries first as an optimisation. // Use alive entries first as an optimisation.
hitObjectContainer.AliveEntries.Select(tuple => tuple.Entry).Where(e => !isAlreadyHit(e)).MinBy(e => e.HitObject.StartTime) hitObjectContainer.AliveEntries.Keys.Where(e => !isAlreadyHit(e)).MinBy(e => e.HitObject.StartTime)
?? hitObjectContainer.Entries.Where(e => !isAlreadyHit(e)).MinBy(e => e.HitObject.StartTime); ?? hitObjectContainer.Entries.Where(e => !isAlreadyHit(e)).MinBy(e => e.HitObject.StartTime);
// In the case there are no non-judged objects, the last hit object should be used instead. // In the case there are no non-judged objects, the last hit object should be used instead.

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.UI
{ {
public IEnumerable<DrawableHitObject> Objects => InternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime); public IEnumerable<DrawableHitObject> Objects => InternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
public IEnumerable<DrawableHitObject> AliveObjects => AliveEntries.Select(pair => pair.Drawable).OrderBy(h => h.HitObject.StartTime); public IEnumerable<DrawableHitObject> AliveObjects => AliveEntries.Values.OrderBy(h => h.HitObject.StartTime);
/// <summary> /// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> is judged. /// Invoked when a <see cref="DrawableHitObject"/> is judged.

View File

@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
// We are not using AliveObjects directly to avoid selection/sorting overhead since we don't care about the order at which positions will be updated. // We are not using AliveObjects directly to avoid selection/sorting overhead since we don't care about the order at which positions will be updated.
foreach (var entry in AliveEntries) foreach (var entry in AliveEntries)
{ {
var obj = entry.Drawable; var obj = entry.Value;
updatePosition(obj, Time.Current); updatePosition(obj, Time.Current);

View File

@ -35,7 +35,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
presets.Add(maxDivisor / candidate); presets.Add(maxDivisor / candidate);
} }
return new BeatDivisorPresetCollection(BeatDivisorType.Custom, presets.Distinct().OrderBy(d => d)); return new BeatDivisorPresetCollection(BeatDivisorType.Custom, presets.Distinct().Order());
} }
} }
} }

View File

@ -169,7 +169,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
InspectorText.Clear(); InspectorText.Clear();
double[] sliderVelocities = EditorBeatmap.HitObjects.OfType<IHasSliderVelocity>().Select(sv => sv.SliderVelocityMultiplier).OrderBy(v => v).ToArray(); double[] sliderVelocities = EditorBeatmap.HitObjects.OfType<IHasSliderVelocity>().Select(sv => sv.SliderVelocityMultiplier).Order().ToArray();
AddHeader("Base velocity (from beatmap setup)"); AddHeader("Base velocity (from beatmap setup)");
AddValue($"{beatmapVelocity:#,0.00}x"); AddValue($"{beatmapVelocity:#,0.00}x");

View File

@ -126,7 +126,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Remove:
Debug.Assert(args.OldItems != null); Debug.Assert(args.OldItems != null);
removeRooms(args.OldItems.Cast<Room>()); // clear operations have a separate path that benefits from async disposal,
// since disposing is quite expensive when performed on a high number of drawables synchronously.
if (args.OldItems.Count == roomFlow.Count)
clearRooms();
else
removeRooms(args.OldItems.Cast<Room>());
break; break;
} }
} }
@ -146,11 +152,20 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
roomFlow.RemoveAll(d => d.Room == r, true); roomFlow.RemoveAll(d => d.Room == r, true);
// selection may have a lease due to being in a sub screen. // selection may have a lease due to being in a sub screen.
if (!SelectedRoom.Disabled) if (SelectedRoom.Value == r && !SelectedRoom.Disabled)
SelectedRoom.Value = null; SelectedRoom.Value = null;
} }
} }
private void clearRooms()
{
roomFlow.Clear();
// selection may have a lease due to being in a sub screen.
if (!SelectedRoom.Disabled)
SelectedRoom.Value = null;
}
private void updateSorting() private void updateSorting()
{ {
foreach (var room in roomFlow) foreach (var room in roomFlow)

View File

@ -198,8 +198,10 @@ namespace osu.Game.Screens.Play
foreach (var nested in playfield.NestedPlayfields) foreach (var nested in playfield.NestedPlayfields)
applyToPlayfield(nested); applyToPlayfield(nested);
foreach (DrawableHitObject obj in playfield.HitObjectContainer.AliveObjects) foreach (var entry in playfield.HitObjectContainer.AliveEntries)
{ {
var obj = entry.Value;
if (appliedObjects.Contains(obj)) if (appliedObjects.Contains(obj))
continue; continue;

View File

@ -4,6 +4,7 @@
#nullable disable #nullable disable
using System.Diagnostics; using System.Diagnostics;
using osu.Game.Extensions;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -30,7 +31,16 @@ namespace osu.Game.Screens.Play
if (!(Room.RoomID.Value is long roomId)) if (!(Room.RoomID.Value is long roomId))
return null; return null;
return new CreateRoomScoreRequest(roomId, PlaylistItem.ID, Game.VersionHash); int beatmapId = Beatmap.Value.BeatmapInfo.OnlineID;
int rulesetId = Ruleset.Value.OnlineID;
if (beatmapId <= 0)
return null;
if (!Ruleset.Value.IsLegacyRuleset())
return null;
return new CreateRoomScoreRequest(roomId, PlaylistItem.ID, Beatmap.Value.BeatmapInfo, rulesetId, Game.VersionHash);
} }
protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token) protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token)

View File

@ -58,6 +58,9 @@ namespace osu.Game.Skinning
{ {
} }
protected override IResourceStore<TextureUpload> CreateTextureLoaderStore(IStorageResourceProvider resources, IResourceStore<byte[]> storage)
=> new LegacyTextureLoaderStore(base.CreateTextureLoaderStore(resources, storage));
protected override void ParseConfigurationStream(Stream stream) protected override void ParseConfigurationStream(Stream stream)
{ {
base.ParseConfigurationStream(stream); base.ParseConfigurationStream(stream);

View File

@ -0,0 +1,95 @@
// 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.IO;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
namespace osu.Game.Skinning
{
public class LegacyTextureLoaderStore : IResourceStore<TextureUpload>
{
private readonly IResourceStore<TextureUpload>? wrappedStore;
public LegacyTextureLoaderStore(IResourceStore<TextureUpload>? wrappedStore)
{
this.wrappedStore = wrappedStore;
}
public TextureUpload Get(string name)
{
var textureUpload = wrappedStore?.Get(name);
if (textureUpload == null)
return null!;
return shouldConvertToGrayscale(name)
? convertToGrayscale(textureUpload)
: textureUpload;
}
public Task<TextureUpload> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken())
{
var textureUpload = wrappedStore?.Get(name);
if (textureUpload == null)
return null!;
return shouldConvertToGrayscale(name)
? Task.Run(() => convertToGrayscale(textureUpload), cancellationToken)
: Task.FromResult(textureUpload);
}
// https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/Graphics/Textures/TextureManager.cs#L91-L96
private static readonly string[] grayscale_sprites =
{
@"taiko-bar-right",
@"taikobigcircle",
@"taikohitcircle",
@"taikohitcircleoverlay"
};
private bool shouldConvertToGrayscale(string name)
{
foreach (string grayscaleSprite in grayscale_sprites)
{
// unfortunately at this level of lookup we can encounter `@2x` scale suffixes in the name,
// so straight equality cannot be used.
if (name.Equals(grayscaleSprite, StringComparison.OrdinalIgnoreCase)
|| name.Equals($@"{grayscaleSprite}@2x", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private TextureUpload convertToGrayscale(TextureUpload textureUpload)
{
var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
// stable uses `0.299 * r + 0.587 * g + 0.114 * b`
// (https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/Graphics/Textures/pTexture.cs#L138-L153)
// which matches mode BT.601 (https://en.wikipedia.org/wiki/Grayscale#Luma_coding_in_video_systems)
image.Mutate(i => i.Grayscale(GrayscaleMode.Bt601));
return new TextureUpload(image);
}
public Stream? GetStream(string name) => wrappedStore?.GetStream(name);
public IEnumerable<string> GetAvailableResources() => wrappedStore?.GetAvailableResources() ?? Array.Empty<string>();
public void Dispose()
{
wrappedStore?.Dispose();
}
}
}

View File

@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -22,10 +21,10 @@ namespace osu.Game.Updater
/// </summary> /// </summary>
public partial class SimpleUpdateManager : UpdateManager public partial class SimpleUpdateManager : UpdateManager
{ {
private string version; private string version = null!;
[Resolved] [Resolved]
private GameHost host { get; set; } private GameHost host { get; set; } = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuGameBase game) private void load(OsuGameBase game)
@ -48,7 +47,7 @@ namespace osu.Game.Updater
version = version.Split('-').First(); version = version.Split('-').First();
string latestTagName = latest.TagName.Split('-').First(); string latestTagName = latest.TagName.Split('-').First();
if (latestTagName != version) if (latestTagName != version && tryGetBestUrl(latest, out string? url))
{ {
Notifications.Post(new SimpleNotification Notifications.Post(new SimpleNotification
{ {
@ -57,7 +56,7 @@ namespace osu.Game.Updater
Icon = FontAwesome.Solid.Download, Icon = FontAwesome.Solid.Download,
Activated = () => Activated = () =>
{ {
host.OpenUrlExternally(getBestUrl(latest)); host.OpenUrlExternally(url);
return true; return true;
} }
}); });
@ -74,9 +73,10 @@ namespace osu.Game.Updater
return false; return false;
} }
private string getBestUrl(GitHubRelease release) private bool tryGetBestUrl(GitHubRelease release, [NotNullWhen(true)] out string? url)
{ {
GitHubAsset bestAsset = null; url = null;
GitHubAsset? bestAsset = null;
switch (RuntimeInfo.OS) switch (RuntimeInfo.OS)
{ {
@ -94,17 +94,23 @@ namespace osu.Game.Updater
break; break;
case RuntimeInfo.Platform.iOS: case RuntimeInfo.Platform.iOS:
// iOS releases are available via testflight. this link seems to work well enough for now. if (release.Assets?.Exists(f => f.Name.EndsWith(".ipa", StringComparison.Ordinal)) == true)
// see https://stackoverflow.com/a/32960501 // iOS releases are available via testflight. this link seems to work well enough for now.
return "itms-beta://beta.itunes.apple.com/v1/app/1447765923"; // see https://stackoverflow.com/a/32960501
url = "itms-beta://beta.itunes.apple.com/v1/app/1447765923";
break;
case RuntimeInfo.Platform.Android: case RuntimeInfo.Platform.Android:
// on our testing device this causes the download to magically disappear. if (release.Assets?.Exists(f => f.Name.EndsWith(".apk", StringComparison.Ordinal)) == true)
//bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".apk")); // on our testing device using the .apk URL causes the download to magically disappear.
url = release.HtmlUrl;
break; break;
} }
return bestAsset?.BrowserDownloadUrl ?? release.HtmlUrl; url ??= bestAsset?.BrowserDownloadUrl;
return url != null;
} }
} }
} }

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