mirror of
https://github.com/ppy/osu.git
synced 2024-11-13 15:27:30 +08:00
Merge branch 'master' into deselect-slider-adds-control-point-bug
This commit is contained in:
commit
6954a185c4
@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Objects.Drawables
|
||||
{
|
||||
if (timeOffset >= 0)
|
||||
// todo: implement judgement logic
|
||||
ApplyResult(r => r.Type = HitResult.Perfect);
|
||||
ApplyResult(HitResult.Perfect);
|
||||
}
|
||||
|
||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||
|
@ -9,7 +9,6 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -49,7 +48,12 @@ namespace osu.Game.Rulesets.Pippidon.Objects.Drawables
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
if (timeOffset >= 0)
|
||||
ApplyResult(r => r.Type = IsHovered ? HitResult.Perfect : HitResult.Miss);
|
||||
{
|
||||
if (IsHovered)
|
||||
ApplyMaxResult();
|
||||
else
|
||||
ApplyMinResult();
|
||||
}
|
||||
}
|
||||
|
||||
protected override double InitialLifetimeOffset => time_preempt;
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -24,7 +23,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Objects.Drawables
|
||||
{
|
||||
if (timeOffset >= 0)
|
||||
// todo: implement judgement logic
|
||||
ApplyResult(r => r.Type = HitResult.Perfect);
|
||||
ApplyMaxResult();
|
||||
}
|
||||
|
||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||
|
@ -10,7 +10,6 @@ using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Pippidon.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -49,7 +48,12 @@ namespace osu.Game.Rulesets.Pippidon.Objects.Drawables
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
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)
|
||||
|
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.131.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.205.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
31
osu.Game.Benchmarks/BenchmarkUnstableRate.cs
Normal file
31
osu.Game.Benchmarks/BenchmarkUnstableRate.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -63,7 +63,12 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
if (CheckPosition == null) return;
|
||||
|
||||
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)
|
||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
private readonly bool isForCurrentRuleset;
|
||||
private readonly double originalOverallDifficulty;
|
||||
|
||||
public override int Version => 20220902;
|
||||
public override int Version => 20230817;
|
||||
|
||||
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
|
@ -375,7 +375,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
/// <returns>The <see cref="PlayfieldType"/> that corresponds to <paramref name="variant"/>.</returns>
|
||||
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()
|
||||
|
@ -265,7 +265,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
if (Tail.AllJudged)
|
||||
{
|
||||
if (Tail.IsHit)
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
ApplyMaxResult();
|
||||
else
|
||||
MissForcefully();
|
||||
}
|
||||
|
@ -25,7 +25,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
if (AllJudged) return;
|
||||
|
||||
ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
if (hit)
|
||||
ApplyMaxResult();
|
||||
else
|
||||
ApplyMinResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
/// <summary>
|
||||
/// Causes this <see cref="DrawableManiaHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
|
||||
/// </summary>
|
||||
public virtual void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
public virtual void MissForcefully() => ApplyMinResult();
|
||||
}
|
||||
|
||||
public abstract partial class DrawableManiaHitObject<TObject> : DrawableManiaHitObject
|
||||
|
@ -89,18 +89,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
if (!userTriggered)
|
||||
{
|
||||
if (!HitObject.HitWindows.CanBeHit(timeOffset))
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
ApplyMinResult();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var result = HitObject.HitWindows.ResultFor(timeOffset);
|
||||
|
||||
if (result == HitResult.None)
|
||||
return;
|
||||
|
||||
result = GetCappedResult(result);
|
||||
|
||||
ApplyResult(r => r.Type = result);
|
||||
ApplyResult(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -1,16 +1,16 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Input.Handlers;
|
||||
@ -59,10 +59,9 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
// Stores the current speed adjustment active in gameplay.
|
||||
private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
|
||||
|
||||
[Resolved]
|
||||
private ISkinSource skin { get; set; }
|
||||
private ISkinSource currentSkin = null!;
|
||||
|
||||
public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)
|
||||
: base(ruleset, beatmap, mods)
|
||||
{
|
||||
BarLines = new BarLineGenerator<BarLine>(Beatmap).BarLines;
|
||||
@ -72,8 +71,12 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(ISkinSource source)
|
||||
{
|
||||
currentSkin = source;
|
||||
currentSkin.SourceChanged += onSkinChange;
|
||||
skinChanged();
|
||||
|
||||
foreach (var mod in Mods.OfType<IApplicableToTrack>())
|
||||
mod.ApplyToTrack(speedAdjustmentTrack);
|
||||
|
||||
@ -109,12 +112,28 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
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()
|
||||
{
|
||||
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;
|
||||
float lengthToHitPosition = 768 - hitPosition;
|
||||
|
||||
@ -139,10 +158,18 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
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 ReplayRecorder CreateReplayRecorder(Score score) => new ManiaReplayRecorder(score);
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (currentSkin.IsNotNull())
|
||||
currentSkin.SourceChanged -= onSkinChange;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
Anchor = 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);
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
accuracyHeatmap.AddPoint(object2.Position, object1.Position, background.ToLocalSpace(e.ScreenSpaceMouseDownPosition), 50);
|
||||
|
@ -136,7 +136,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
if (auto && !userTriggered && timeOffset > hitOffset && CheckHittable?.Invoke(this, Time.Current, HitResult.Great) == ClickAction.Hit)
|
||||
{
|
||||
// force success
|
||||
ApplyResult(r => r.Type = HitResult.Great);
|
||||
ApplyResult(HitResult.Great);
|
||||
}
|
||||
else
|
||||
base.CheckForResult(userTriggered, timeOffset);
|
||||
|
@ -208,7 +208,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
if (shouldHit && !userTriggered && timeOffset >= 0)
|
||||
{
|
||||
// force success
|
||||
ApplyResult(r => r.Type = HitResult.Great);
|
||||
ApplyResult(HitResult.Great);
|
||||
}
|
||||
else
|
||||
base.CheckForResult(userTriggered, timeOffset);
|
||||
|
@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
// These sections will not contribute to the difficulty.
|
||||
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
|
||||
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.
|
||||
// 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;
|
||||
weight *= DecayWeight;
|
||||
|
@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight) };
|
||||
public override bool Ranked => true;
|
||||
|
||||
private DrawableOsuBlinds blinds = null!;
|
||||
|
||||
|
@ -75,8 +75,10 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
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)
|
||||
{
|
||||
case DrawableHitCircle circle:
|
||||
|
@ -49,8 +49,10 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
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)
|
||||
{
|
||||
case DrawableHitCircle circle:
|
||||
|
@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
if (!slider.HeadCircle.IsHit)
|
||||
handleHitCircle(slider.HeadCircle);
|
||||
|
||||
requiresHold |= slider.SliderInputManager.IsMouseInFollowArea(true);
|
||||
requiresHold |= slider.SliderInputManager.IsMouseInFollowArea(slider.Tracking.Value);
|
||||
break;
|
||||
|
||||
case DrawableSpinner spinner:
|
||||
|
@ -48,8 +48,10 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
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);
|
||||
|
||||
if (drawable.HitObject is Slider thisSlider)
|
||||
|
@ -155,7 +155,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
if (!userTriggered)
|
||||
{
|
||||
if (!HitObject.HitWindows.CanBeHit(timeOffset))
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
ApplyMinResult();
|
||||
|
||||
return;
|
||||
}
|
||||
@ -169,19 +169,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
if (result == HitResult.None || clickAction != ClickAction.Hit)
|
||||
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;
|
||||
|
||||
// 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);
|
||||
circleResult.CursorPositionAtHit = HitObject.StackedPosition + (localMousePosition - DrawSize / 2);
|
||||
}
|
||||
|
||||
circleResult.Type = result;
|
||||
});
|
||||
circleResult.Type = state.result;
|
||||
circleResult.CursorPositionAtHit = state.position;
|
||||
}, (result, hitPosition));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -100,12 +100,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
/// <summary>
|
||||
/// Causes this <see cref="DrawableOsuHitObject"/> to get hit, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
|
||||
/// </summary>
|
||||
public void HitForcefully() => ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
public void HitForcefully() => ApplyMaxResult();
|
||||
|
||||
/// <summary>
|
||||
/// Causes this <see cref="DrawableOsuHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
|
||||
/// </summary>
|
||||
public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
public void MissForcefully() => ApplyMinResult();
|
||||
|
||||
private RectangleF parentScreenSpaceRectangle => ((DrawableOsuHitObject)ParentHitObject)?.parentScreenSpaceRectangle ?? Parent!.ScreenSpaceDrawQuad.AABBFloat;
|
||||
|
||||
|
@ -292,10 +292,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
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.
|
||||
ApplyResult(r =>
|
||||
ApplyResult(static (r, hitObject) =>
|
||||
{
|
||||
int totalTicks = NestedHitObjects.Count;
|
||||
int hitTicks = NestedHitObjects.Count(h => h.IsHit);
|
||||
int totalTicks = hitObject.NestedHitObjects.Count;
|
||||
int hitTicks = hitObject.NestedHitObjects.Count(h => h.IsHit);
|
||||
|
||||
if (hitTicks == totalTicks)
|
||||
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.
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -258,15 +258,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
foreach (var tick in ticks.Where(t => !t.Result.HasResult))
|
||||
tick.TriggerResult(false);
|
||||
|
||||
ApplyResult(r =>
|
||||
ApplyResult(static (r, hitObject) =>
|
||||
{
|
||||
if (Progress >= 1)
|
||||
var spinner = (DrawableSpinner)hitObject;
|
||||
if (spinner.Progress >= 1)
|
||||
r.Type = HitResult.Great;
|
||||
else if (Progress > .9)
|
||||
else if (spinner.Progress > .9)
|
||||
r.Type = HitResult.Ok;
|
||||
else if (Progress > .75)
|
||||
else if (spinner.Progress > .75)
|
||||
r.Type = HitResult.Meh;
|
||||
else if (Time.Current >= HitObject.EndTime)
|
||||
else if (spinner.Time.Current >= spinner.HitObject.EndTime)
|
||||
r.Type = r.Judgement.MinResult;
|
||||
});
|
||||
}
|
||||
|
@ -35,6 +35,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
/// Apply a judgement result.
|
||||
/// </summary>
|
||||
/// <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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,6 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
public class TailJudgement : SliderEndJudgement
|
||||
{
|
||||
public override HitResult MaxResult => HitResult.SliderTailHit;
|
||||
public override HitResult MinResult => HitResult.IgnoreMiss;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -191,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
||||
|
||||
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.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 |
@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
double difficulty = 0;
|
||||
double weight = 1;
|
||||
|
||||
foreach (double strain in peaks.OrderByDescending(d => d))
|
||||
foreach (double strain in peaks.OrderDescending())
|
||||
{
|
||||
difficulty += strain * weight;
|
||||
weight *= 0.9;
|
||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
private const double difficulty_multiplier = 1.35;
|
||||
|
||||
public override int Version => 20220902;
|
||||
public override int Version => 20221107;
|
||||
|
||||
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
|
@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
if (timeOffset < 0)
|
||||
return;
|
||||
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
ApplyMaxResult();
|
||||
}
|
||||
|
||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||
@ -192,7 +192,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
if (!ParentHitObject.Judged)
|
||||
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;
|
||||
|
@ -49,14 +49,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
if (!userTriggered)
|
||||
{
|
||||
if (timeOffset > HitObject.HitWindow)
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
ApplyMinResult();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.Abs(timeOffset) > HitObject.HitWindow)
|
||||
return;
|
||||
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
ApplyMaxResult();
|
||||
}
|
||||
|
||||
public override void OnKilled()
|
||||
@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
base.OnKilled();
|
||||
|
||||
if (Time.Current > HitObject.GetEndTime() && !Judged)
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
ApplyMinResult();
|
||||
}
|
||||
|
||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||
@ -105,7 +105,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
if (!ParentHitObject.Judged)
|
||||
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;
|
||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
ApplyMaxResult();
|
||||
}
|
||||
|
||||
protected override void LoadSamples()
|
||||
|
@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
if (!userTriggered)
|
||||
{
|
||||
if (!HitObject.HitWindows.CanBeHit(timeOffset))
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
ApplyMinResult();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -108,9 +108,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
return;
|
||||
|
||||
if (!validActionPressed)
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
ApplyMinResult();
|
||||
else
|
||||
ApplyResult(r => r.Type = result);
|
||||
ApplyResult(result);
|
||||
}
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
|
||||
@ -209,19 +209,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
if (!ParentHitObject.Result.IsHit)
|
||||
{
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
ApplyMinResult();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userTriggered)
|
||||
{
|
||||
if (timeOffset - ParentHitObject.Result.TimeOffset > SECOND_HIT_WINDOW)
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
ApplyMinResult();
|
||||
return;
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -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.
|
||||
// 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())
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
ApplyMinResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
if (numHits == HitObject.RequiredHits)
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
ApplyMaxResult();
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -227,7 +227,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
tick.TriggerResult(false);
|
||||
}
|
||||
|
||||
ApplyResult(r => r.Type = numHits == HitObject.RequiredHits ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
if (numHits == HitObject.RequiredHits)
|
||||
ApplyMaxResult();
|
||||
else
|
||||
ApplyMinResult();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
public void TriggerResult(bool hit)
|
||||
{
|
||||
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)
|
||||
|
@ -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.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Taiko.Configuration;
|
||||
|
||||
@ -27,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
{
|
||||
new SettingsEnumDropdown<TaikoTouchControlScheme>
|
||||
{
|
||||
LabelText = "Touch control scheme",
|
||||
LabelText = RulesetSettingsStrings.TouchControlScheme,
|
||||
Current = config.GetBindable<TaikoTouchControlScheme>(TaikoRulesetSetting.TouchControlScheme)
|
||||
}
|
||||
};
|
||||
|
@ -216,7 +216,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
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)
|
||||
{
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -31,8 +29,8 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
{
|
||||
public partial class ScoreProcessorTest
|
||||
{
|
||||
private ScoreProcessor scoreProcessor;
|
||||
private IBeatmap beatmap;
|
||||
private ScoreProcessor scoreProcessor = null!;
|
||||
private IBeatmap beatmap = null!;
|
||||
|
||||
[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.LargeTickMiss, HitResult.LargeTickHit, 0)]
|
||||
[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.LargeBonus, HitResult.LargeBonus, 1_000_150)]
|
||||
[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.LargeTickMiss, HitResult.LargeTickHit, 0)]
|
||||
[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.LargeBonus, HitResult.LargeBonus, 100_015)]
|
||||
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.SmallTickHit, HitResult.SmallTickMiss)]
|
||||
[TestCase(HitResult.LargeTickHit, HitResult.LargeTickMiss)]
|
||||
[TestCase(HitResult.SliderTailHit, HitResult.LargeTickMiss)]
|
||||
[TestCase(HitResult.SliderTailHit, HitResult.IgnoreMiss)]
|
||||
[TestCase(HitResult.SmallBonus, HitResult.IgnoreMiss)]
|
||||
[TestCase(HitResult.LargeBonus, HitResult.IgnoreMiss)]
|
||||
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 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();
|
||||
|
||||
|
@ -431,7 +431,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
if (timeOffset > HitObject.Duration)
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
ApplyMaxResult();
|
||||
}
|
||||
|
||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||
@ -468,7 +468,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public override void 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);
|
||||
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);
|
||||
if (timeOffset >= 0)
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
ApplyMaxResult();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,6 +64,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("select first room", () => container.Rooms.First().TriggerClick());
|
||||
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]
|
||||
|
@ -2,14 +2,15 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
public partial class TestSceneDashboardOverlay : OsuTestScene
|
||||
{
|
||||
protected override bool UseOnlineAPI => true;
|
||||
|
||||
private readonly DashboardOverlay overlay;
|
||||
|
||||
public TestSceneDashboardOverlay()
|
||||
@ -17,6 +18,30 @@ namespace osu.Game.Tests.Visual.Online
|
||||
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]
|
||||
public void TestShow()
|
||||
{
|
||||
|
@ -206,6 +206,12 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Total = 50
|
||||
},
|
||||
SupportLevel = 2,
|
||||
Location = "Somewhere",
|
||||
Interests = "Rhythm games",
|
||||
Occupation = "Gamer",
|
||||
Twitter = "test_user",
|
||||
Discord = "test_user",
|
||||
Website = "https://google.com",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +45,6 @@ namespace osu.Game.Tournament.IO
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -266,8 +266,8 @@ namespace osu.Game.Beatmaps
|
||||
if (!base.CanReuseExisting(existing, import))
|
||||
return false;
|
||||
|
||||
var existingIds = existing.Beatmaps.Select(b => b.OnlineID).OrderBy(i => i);
|
||||
var importIds = import.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).Order();
|
||||
|
||||
// force re-import if we are not in a sane state.
|
||||
return existing.OnlineID == import.OnlineID && existingIds.SequenceEqual(importIds);
|
||||
|
@ -74,7 +74,7 @@ namespace osu.Game.Collections
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (int i in changes.DeletedIndices.OrderByDescending(i => i))
|
||||
foreach (int i in changes.DeletedIndices.OrderDescending())
|
||||
filters.RemoveAt(i + 1);
|
||||
|
||||
foreach (int i in changes.InsertedIndices)
|
||||
|
@ -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.
|
||||
// or alternatively doing a faster hash check. either of these require database changes and reprocessing of existing files.
|
||||
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))
|
||||
{
|
||||
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();
|
||||
|
||||
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))
|
||||
s.CopyTo(hashable);
|
||||
|
@ -64,6 +64,21 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
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>
|
||||
/// "Offset wizard"
|
||||
/// </summary>
|
||||
|
@ -29,6 +29,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
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>
|
||||
/// "Memory"
|
||||
/// </summary>
|
||||
|
@ -51,6 +51,31 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString Items(int arg0) => new TranslatableString(getKey(@"items"), @"{0} item(s)", arg0);
|
||||
|
||||
/// <summary>
|
||||
/// "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>
|
||||
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 "hard links" 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}";
|
||||
}
|
||||
}
|
||||
|
@ -159,6 +159,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
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}";
|
||||
}
|
||||
}
|
||||
|
@ -113,6 +113,28 @@ Please try changing your audio device to a working setting.");
|
||||
/// </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.");
|
||||
|
||||
/// <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}";
|
||||
}
|
||||
}
|
||||
|
@ -84,6 +84,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
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}";
|
||||
}
|
||||
}
|
||||
|
@ -203,6 +203,7 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
Ruleset = new RulesetInfo { OnlineID = RulesetID },
|
||||
Passed = Passed,
|
||||
TotalScore = TotalScore,
|
||||
LegacyTotalScore = LegacyTotalScore,
|
||||
Accuracy = Accuracy,
|
||||
MaxCombo = MaxCombo,
|
||||
Rank = Rank,
|
||||
|
@ -23,9 +23,12 @@ namespace osu.Game.Online.Multiplayer
|
||||
|
||||
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);
|
||||
}
|
||||
else
|
||||
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.API;
|
||||
|
||||
namespace osu.Game.Online
|
||||
@ -31,6 +32,12 @@ namespace osu.Game.Online
|
||||
private CancellationTokenSource connectCancelSource = new CancellationTokenSource();
|
||||
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>
|
||||
/// Constructs a new <see cref="PersistentEndpointClientConnector"/>.
|
||||
/// </summary>
|
||||
@ -78,6 +85,8 @@ namespace osu.Game.Online
|
||||
private async Task connect()
|
||||
{
|
||||
cancelExistingConnect();
|
||||
// reset retry delay to default.
|
||||
retryDelay = 3000;
|
||||
|
||||
if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
|
||||
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>
|
||||
private async Task handleErrorAndDelay(Exception exception, CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.Log($"{ClientName} connect attempt failed: {exception.Message}", LoggingTarget.Network);
|
||||
await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
|
||||
// random stagger factor to avoid mass incidental synchronisation
|
||||
// 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>
|
||||
|
@ -1,8 +1,10 @@
|
||||
// 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.Globalization;
|
||||
using System.Net.Http;
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
|
||||
namespace osu.Game.Online.Rooms
|
||||
@ -11,12 +13,16 @@ namespace osu.Game.Online.Rooms
|
||||
{
|
||||
private readonly long roomId;
|
||||
private readonly long playlistItemId;
|
||||
private readonly BeatmapInfo beatmapInfo;
|
||||
private readonly int rulesetId;
|
||||
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.playlistItemId = playlistItemId;
|
||||
this.beatmapInfo = beatmapInfo;
|
||||
this.rulesetId = rulesetId;
|
||||
this.versionHash = versionHash;
|
||||
}
|
||||
|
||||
@ -25,6 +31,8 @@ namespace osu.Game.Online.Rooms
|
||||
var req = base.CreateWebRequest();
|
||||
req.Method = HttpMethod.Post;
|
||||
req.AddParameter("version_hash", versionHash);
|
||||
req.AddParameter("beatmap_hash", beatmapInfo.MD5Hash);
|
||||
req.AddParameter("ruleset_id", rulesetId.ToString(CultureInfo.InvariantCulture));
|
||||
return req;
|
||||
}
|
||||
|
||||
|
@ -127,18 +127,17 @@ namespace osu.Game.Overlays.FirstRunSetup
|
||||
|
||||
if (available)
|
||||
{
|
||||
copyInformation.Text =
|
||||
"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("Learn more about how \"hard links\" work", LinkAction.OpenWiki, @"Client/Release_stream/Lazer/File_storage#via-hard-links");
|
||||
copyInformation.Text = FirstRunOverlayImportFromStableScreenStrings.DataMigrationNoExtraSpace;
|
||||
copyInformation.AddLink(FirstRunOverlayImportFromStableScreenStrings.LearnAboutHardLinks, LinkAction.OpenWiki, @"Client/Release_stream/Lazer/File_storage#via-hard-links");
|
||||
}
|
||||
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
|
||||
{
|
||||
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). "
|
||||
: "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.SecondCopyWillBeMadeWindows
|
||||
: FirstRunOverlayImportFromStableScreenStrings.SecondCopyWillBeMadeOtherPlatforms;
|
||||
copyInformation.AddText(@" "); // just to ensure correct spacing
|
||||
copyInformation.AddLink(GeneralSettingsStrings.ChangeFolderLocation, () =>
|
||||
{
|
||||
game?.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen()));
|
||||
|
@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Direction = FillDirection.Horizontal,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(7),
|
||||
Children = new Drawable[]
|
||||
|
@ -122,7 +122,7 @@ namespace osu.Game.Overlays.Music
|
||||
foreach (int i in changes.InsertedIndices)
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -144,8 +144,8 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
|
||||
bool anyInfoAdded = false;
|
||||
|
||||
anyInfoAdded |= tryAddInfo(FontAwesome.Solid.MapMarker, user.Location);
|
||||
anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Heart, user.Interests);
|
||||
anyInfoAdded |= tryAddInfo(FontAwesome.Solid.MapMarkerAlt, user.Location);
|
||||
anyInfoAdded |= tryAddInfo(FontAwesome.Regular.Heart, user.Interests);
|
||||
anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Suitcase, user.Occupation);
|
||||
|
||||
if (anyInfoAdded)
|
||||
@ -171,7 +171,7 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
|
||||
bottomLinkContainer.AddIcon(icon, text =>
|
||||
{
|
||||
text.Font = text.Font.With(size: 10);
|
||||
text.Font = text.Font.With(icon.Family, 10, icon.Weight);
|
||||
text.Colour = iconColour;
|
||||
});
|
||||
|
||||
|
@ -1,13 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Users;
|
||||
|
||||
@ -19,8 +17,8 @@ namespace osu.Game.Overlays.Rankings
|
||||
|
||||
public Bindable<CountryCode> Country => countryFilter.Current;
|
||||
|
||||
private OverlayRulesetSelector rulesetSelector;
|
||||
private CountryFilter countryFilter;
|
||||
private OverlayRulesetSelector rulesetSelector = null!;
|
||||
private CountryFilter countryFilter = null!;
|
||||
|
||||
protected override OverlayTitle CreateTitle() => new RankingsTitle();
|
||||
|
||||
@ -39,5 +37,30 @@ namespace osu.Game.Overlays.Rankings
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -91,7 +92,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
|
||||
applySuggestion = new RoundedButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = "Apply suggested offset",
|
||||
Text = AudioSettingsStrings.ApplySuggestedOffset,
|
||||
Action = () =>
|
||||
{
|
||||
if (SuggestedOffset.Value.HasValue)
|
||||
@ -155,8 +156,8 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
|
||||
private void updateHintText()
|
||||
{
|
||||
hintText.Text = SuggestedOffset.Value == null
|
||||
? @"Play a few beatmaps to receive a suggested offset!"
|
||||
: $@"Based on the last {averageHitErrorHistory.Count} play(s), the suggested offset is {SuggestedOffset.Value:N0} ms.";
|
||||
? AudioSettingsStrings.SuggestedOffsetNote
|
||||
: AudioSettingsStrings.SuggestedOffsetValueReceived(averageHitErrorHistory.Count, SuggestedOffset.Value.ToLocalisableString(@"N0"));
|
||||
applySuggestion.Enabled.Value = SuggestedOffset.Value != null;
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings
|
||||
},
|
||||
new SettingsButton
|
||||
{
|
||||
Text = @"Run latency certifier",
|
||||
Text = DebugSettingsStrings.RunLatencyCertifier,
|
||||
Action = () => performer?.PerformFromScreen(menu => menu.Push(new LatencyCertifierScreen()))
|
||||
}
|
||||
};
|
||||
|
@ -115,7 +115,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
},
|
||||
safeAreaConsiderationsCheckbox = new SettingsCheckbox
|
||||
{
|
||||
LabelText = "Shrink game to avoid cameras and notches",
|
||||
LabelText = GraphicsSettingsStrings.ShrinkGameToSafeArea,
|
||||
Current = osuConfig.GetBindable<bool>(OsuSetting.SafeAreaConsiderations),
|
||||
},
|
||||
new SettingsSlider<float, UIScaleSlider>
|
||||
|
@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
{
|
||||
LabelText = GraphicsSettingsStrings.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" },
|
||||
},
|
||||
// TODO: this needs to be a custom dropdown at some point
|
||||
|
@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
||||
|
||||
// Difficulty is the weighted sum of the highest strains from every section.
|
||||
// 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;
|
||||
weight *= DecayWeight;
|
||||
|
@ -31,8 +31,8 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
var startTimes = context.Beatmap.HitObjects.Select(ho => ho.StartTime).OrderBy(x => x).ToList();
|
||||
var endTimes = context.Beatmap.HitObjects.Select(ho => ho.GetEndTime()).OrderBy(x => x).ToList();
|
||||
var startTimes = context.Beatmap.HitObjects.Select(ho => ho.StartTime).Order().ToList();
|
||||
var endTimes = context.Beatmap.HitObjects.Select(ho => ho.GetEndTime()).Order().ToList();
|
||||
|
||||
foreach (var breakPeriod in context.Beatmap.Breaks)
|
||||
{
|
||||
|
@ -73,9 +73,11 @@ namespace osu.Game.Rulesets.Judgements
|
||||
return HitResult.SmallTickMiss;
|
||||
|
||||
case HitResult.LargeTickHit:
|
||||
case HitResult.SliderTailHit:
|
||||
return HitResult.LargeTickMiss;
|
||||
|
||||
case HitResult.SliderTailHit:
|
||||
return HitResult.IgnoreMiss;
|
||||
|
||||
default:
|
||||
return HitResult.Miss;
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Judgements
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// <remarks>
|
||||
/// This is used instead of <see cref="TimeAbsolute"/> to check whether this <see cref="JudgementResult"/> should be reverted.
|
||||
|
@ -31,6 +31,8 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public override bool RequiresConfiguration => false;
|
||||
|
||||
public override bool Ranked => true;
|
||||
|
||||
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>))]
|
||||
|
@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override IconUsage? Icon => OsuIcon.ModDoubleTime;
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
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))]
|
||||
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(1.5)
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override IconUsage? Icon => OsuIcon.ModHalftime;
|
||||
public override ModType Type => ModType.DifficultyReduction;
|
||||
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))]
|
||||
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(0.75)
|
||||
|
@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override IconUsage? Icon => FontAwesome.Solid.EyeSlash;
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override bool Ranked => true;
|
||||
|
||||
/// <summary>
|
||||
/// Slightly higher than the cutoff for <see cref="Drawable.IsPresent"/>.
|
||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override double ScoreMultiplier => 1;
|
||||
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();
|
||||
|
||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override LocalisableString Description => "Miss and fail.";
|
||||
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();
|
||||
|
||||
|
@ -683,17 +683,31 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
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>
|
||||
/// Applies the <see cref="Result"/> of this <see cref="DrawableHitObject"/>, notifying responders such as
|
||||
/// the <see cref="ScoreProcessor"/> of the <see cref="JudgementResult"/>.
|
||||
/// </summary>
|
||||
/// <param name="application">The callback that applies changes to the <see cref="JudgementResult"/>.</param>
|
||||
protected void ApplyResult(Action<JudgementResult> application)
|
||||
/// <param name="application">The callback that applies changes to the <see cref="JudgementResult"/>. Using a `static` delegate is recommended to avoid allocation overhead.</param>
|
||||
/// <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)
|
||||
throw new InvalidOperationException("Cannot apply result on a hitobject that already has a result.");
|
||||
|
||||
application?.Invoke(Result);
|
||||
application?.Invoke(Result, state);
|
||||
|
||||
if (!Result.HasResult)
|
||||
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"/>.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <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.
|
||||
|
@ -4,9 +4,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.ListExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Performance;
|
||||
using osu.Framework.Lists;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Pooling
|
||||
{
|
||||
@ -35,7 +37,7 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
/// <remarks>
|
||||
/// The enumeration order is undefined.
|
||||
/// </remarks>
|
||||
public IEnumerable<(TEntry Entry, TDrawable Drawable)> AliveEntries => aliveDrawableMap.Select(x => (x.Key, x.Value));
|
||||
public readonly SlimReadOnlyDictionaryWrapper<TEntry, TDrawable> AliveEntries;
|
||||
|
||||
/// <summary>
|
||||
/// 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.EntryBecameDead += entryBecameDead;
|
||||
lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary;
|
||||
|
||||
AliveEntries = aliveDrawableMap.AsSlimReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -107,7 +107,7 @@ namespace osu.Game.Rulesets
|
||||
}
|
||||
}
|
||||
|
||||
availableRulesets.AddRange(detachedRulesets.OrderBy(r => r));
|
||||
availableRulesets.AddRange(detachedRulesets.Order());
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,9 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// <summary>
|
||||
/// Calculates the "unstable rate" for a sequence of <see cref="HitEvent"/>s.
|
||||
/// </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>
|
||||
/// 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.
|
||||
@ -21,9 +24,28 @@ namespace osu.Game.Rulesets.Scoring
|
||||
{
|
||||
Debug.Assert(hitEvents.All(ev => ev.GameplayRate != null));
|
||||
|
||||
// Division by gameplay rate is to account for TimeOffset scaling with gameplay rate.
|
||||
double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset / ev.GameplayRate!.Value).ToArray();
|
||||
return 10 * standardDeviation(timeOffsets);
|
||||
int count = 0;
|
||||
double mean = 0;
|
||||
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>
|
||||
@ -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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -138,7 +138,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
ComboBreak,
|
||||
|
||||
/// <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>
|
||||
[EnumMember(Value = "slider_tail_hit")]
|
||||
[Order(8)]
|
||||
|
@ -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.
|
||||
var candidate =
|
||||
// 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);
|
||||
|
||||
// In the case there are no non-judged objects, the last hit object should be used instead.
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
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>
|
||||
/// Invoked when a <see cref="DrawableHitObject"/> is judged.
|
||||
|
@ -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.
|
||||
foreach (var entry in AliveEntries)
|
||||
{
|
||||
var obj = entry.Drawable;
|
||||
var obj = entry.Value;
|
||||
|
||||
updatePosition(obj, Time.Current);
|
||||
|
||||
|
@ -35,7 +35,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
presets.Add(maxDivisor / candidate);
|
||||
}
|
||||
|
||||
return new BeatDivisorPresetCollection(BeatDivisorType.Custom, presets.Distinct().OrderBy(d => d));
|
||||
return new BeatDivisorPresetCollection(BeatDivisorType.Custom, presets.Distinct().Order());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -169,7 +169,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
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)");
|
||||
AddValue($"{beatmapVelocity:#,0.00}x");
|
||||
|
@ -126,7 +126,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -146,11 +152,20 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
roomFlow.RemoveAll(d => d.Room == r, true);
|
||||
|
||||
// selection may have a lease due to being in a sub screen.
|
||||
if (!SelectedRoom.Disabled)
|
||||
if (SelectedRoom.Value == r && !SelectedRoom.Disabled)
|
||||
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()
|
||||
{
|
||||
foreach (var room in roomFlow)
|
||||
|
@ -198,8 +198,10 @@ namespace osu.Game.Screens.Play
|
||||
foreach (var nested in playfield.NestedPlayfields)
|
||||
applyToPlayfield(nested);
|
||||
|
||||
foreach (DrawableHitObject obj in playfield.HitObjectContainer.AliveObjects)
|
||||
foreach (var entry in playfield.HitObjectContainer.AliveEntries)
|
||||
{
|
||||
var obj = entry.Value;
|
||||
|
||||
if (appliedObjects.Contains(obj))
|
||||
continue;
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Diagnostics;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Scoring;
|
||||
@ -30,7 +31,16 @@ namespace osu.Game.Screens.Play
|
||||
if (!(Room.RoomID.Value is long roomId))
|
||||
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)
|
||||
|
@ -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)
|
||||
{
|
||||
base.ParseConfigurationStream(stream);
|
||||
|
95
osu.Game/Skinning/LegacyTextureLoaderStore.cs
Normal file
95
osu.Game/Skinning/LegacyTextureLoaderStore.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
@ -22,10 +21,10 @@ namespace osu.Game.Updater
|
||||
/// </summary>
|
||||
public partial class SimpleUpdateManager : UpdateManager
|
||||
{
|
||||
private string version;
|
||||
private string version = null!;
|
||||
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
private GameHost host { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuGameBase game)
|
||||
@ -48,7 +47,7 @@ namespace osu.Game.Updater
|
||||
version = version.Split('-').First();
|
||||
string latestTagName = latest.TagName.Split('-').First();
|
||||
|
||||
if (latestTagName != version)
|
||||
if (latestTagName != version && tryGetBestUrl(latest, out string? url))
|
||||
{
|
||||
Notifications.Post(new SimpleNotification
|
||||
{
|
||||
@ -57,7 +56,7 @@ namespace osu.Game.Updater
|
||||
Icon = FontAwesome.Solid.Download,
|
||||
Activated = () =>
|
||||
{
|
||||
host.OpenUrlExternally(getBestUrl(latest));
|
||||
host.OpenUrlExternally(url);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@ -74,9 +73,10 @@ namespace osu.Game.Updater
|
||||
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)
|
||||
{
|
||||
@ -94,17 +94,23 @@ namespace osu.Game.Updater
|
||||
break;
|
||||
|
||||
case RuntimeInfo.Platform.iOS:
|
||||
// iOS releases are available via testflight. this link seems to work well enough for now.
|
||||
// see https://stackoverflow.com/a/32960501
|
||||
return "itms-beta://beta.itunes.apple.com/v1/app/1447765923";
|
||||
if (release.Assets?.Exists(f => f.Name.EndsWith(".ipa", StringComparison.Ordinal)) == true)
|
||||
// iOS releases are available via testflight. this link seems to work well enough for now.
|
||||
// see https://stackoverflow.com/a/32960501
|
||||
url = "itms-beta://beta.itunes.apple.com/v1/app/1447765923";
|
||||
|
||||
break;
|
||||
|
||||
case RuntimeInfo.Platform.Android:
|
||||
// on our testing device this causes the download to magically disappear.
|
||||
//bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".apk"));
|
||||
if (release.Assets?.Exists(f => f.Name.EndsWith(".apk", StringComparison.Ordinal)) == true)
|
||||
// on our testing device using the .apk URL causes the download to magically disappear.
|
||||
url = release.HtmlUrl;
|
||||
|
||||
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
Loading…
Reference in New Issue
Block a user