1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 19:27:24 +08:00

Merge branch 'master' into mod-overlay/preset-panel

This commit is contained in:
Bartłomiej Dach 2022-07-22 19:23:09 +02:00
commit d0e15d04cc
No known key found for this signature in database
GPG Key ID: BCECCD4FA41F6497
35 changed files with 991 additions and 202 deletions

View File

@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
private float halfCatcherWidth;
public override int Version => 20220701;
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
{

View File

@ -31,6 +31,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private readonly bool isForCurrentRuleset;
private readonly double originalOverallDifficulty;
public override int Version => 20220701;
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
{

View File

@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private const double difficulty_multiplier = 0.0675;
private double hitWindowGreat;
public override int Version => 20220701;
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
{

View File

@ -23,13 +23,29 @@ namespace osu.Game.Rulesets.Taiko.Tests
protected DrawableTaikoRuleset DrawableRuleset { get; private set; }
protected Container PlayfieldContainer { get; private set; }
private ControlPointInfo controlPointInfo { get; set; }
[BackgroundDependencyLoader]
private void load()
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint());
IWorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap
IWorkingBeatmap beatmap = CreateWorkingBeatmap(CreateBeatmap(new TaikoRuleset().RulesetInfo));
Add(PlayfieldContainer = new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
Height = DEFAULT_PLAYFIELD_CONTAINER_HEIGHT,
Children = new[] { DrawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset)) }
});
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
return new Beatmap
{
HitObjects = new List<HitObject> { new Hit { Type = HitType.Centre } },
BeatmapInfo = new BeatmapInfo
@ -41,19 +57,10 @@ namespace osu.Game.Rulesets.Taiko.Tests
Title = @"Sample Beatmap",
Author = { Username = @"peppy" },
},
Ruleset = new TaikoRuleset().RulesetInfo
Ruleset = ruleset
},
ControlPointInfo = controlPointInfo
});
Add(PlayfieldContainer = new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
Height = DEFAULT_PLAYFIELD_CONTAINER_HEIGHT,
Children = new[] { DrawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(new TaikoRuleset().RulesetInfo)) }
});
};
}
}
}

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200),
Child = new InputDrum(playfield.HitObjectContainer)
Child = new InputDrum()
}
});
}

View File

@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
{
[TestFixture]
public class TestSceneDrumTouchInputArea : OsuTestScene
{
private DrumTouchInputArea drumTouchInputArea = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create drum", () =>
{
Child = new TaikoInputManager(new TaikoRuleset().RulesetInfo)
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new InputDrum
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Height = 0.2f,
},
drumTouchInputArea = new DrumTouchInputArea
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
},
},
};
});
}
[Test]
public void TestDrum()
{
AddStep("show drum", () => drumTouchInputArea.Show());
}
}
}

View File

@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private const double colour_skill_multiplier = 0.01;
private const double stamina_skill_multiplier = 0.021;
public override int Version => 20220701;
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
{

View File

@ -10,8 +10,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Skinning;
using osuTK;
@ -115,9 +113,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public readonly Sprite Rim;
public readonly Sprite Centre;
[Resolved]
private DrumSampleTriggerSource sampleTriggerSource { get; set; }
public LegacyHalfDrum(bool flipped)
{
Masking = true;
@ -152,12 +147,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
if (e.Action == CentreAction)
{
target = Centre;
sampleTriggerSource.Play(HitType.Centre);
}
else if (e.Action == RimAction)
{
target = Rim;
sampleTriggerSource.Play(HitType.Rim);
}
if (target != null)

View File

@ -4,11 +4,13 @@
#nullable disable
using System.ComponentModel;
using osu.Framework.Allocation;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko
{
[Cached] // Used for touch input, see DrumTouchInputArea.
public class TaikoInputManager : RulesetInputManager<TaikoAction>
{
public TaikoInputManager(RulesetInfo ruleset)

View File

@ -8,18 +8,18 @@ using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Framework.Input;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Input.Handlers;
using osu.Game.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Rulesets.Timing;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Scoring;
using osu.Game.Skinning;
@ -56,6 +56,8 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.X,
Depth = float.MaxValue
});
KeyBindingInputManager.Add(new DrumTouchInputArea());
}
protected override void UpdateAfterChildren()

View File

@ -0,0 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko.UI
{
internal class DrumSamplePlayer : CompositeDrawable, IKeyBindingHandler<TaikoAction>
{
private readonly DrumSampleTriggerSource leftRimSampleTriggerSource;
private readonly DrumSampleTriggerSource leftCentreSampleTriggerSource;
private readonly DrumSampleTriggerSource rightCentreSampleTriggerSource;
private readonly DrumSampleTriggerSource rightRimSampleTriggerSource;
public DrumSamplePlayer(HitObjectContainer hitObjectContainer)
{
InternalChildren = new Drawable[]
{
leftRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
leftCentreSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
rightCentreSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
rightRimSampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer),
};
}
public bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
{
switch (e.Action)
{
case TaikoAction.LeftRim:
leftRimSampleTriggerSource.Play(HitType.Rim);
break;
case TaikoAction.LeftCentre:
leftCentreSampleTriggerSource.Play(HitType.Centre);
break;
case TaikoAction.RightCentre:
rightCentreSampleTriggerSource.Play(HitType.Centre);
break;
case TaikoAction.RightRim:
rightRimSampleTriggerSource.Play(HitType.Rim);
break;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<TaikoAction> e)
{
}
}
}

View File

@ -0,0 +1,243 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.UI
{
/// <summary>
/// An overlay that captures and displays osu!taiko mouse and touch input.
/// </summary>
public class DrumTouchInputArea : VisibilityContainer
{
// visibility state affects our child. we always want to handle input.
public override bool PropagatePositionalInputSubTree => true;
public override bool PropagateNonPositionalInputSubTree => true;
private KeyBindingContainer<TaikoAction> keyBindingContainer = null!;
private readonly Dictionary<object, TaikoAction> trackedActions = new Dictionary<object, TaikoAction>();
private Container mainContent = null!;
private QuarterCircle leftCentre = null!;
private QuarterCircle rightCentre = null!;
private QuarterCircle leftRim = null!;
private QuarterCircle rightRim = null!;
[BackgroundDependencyLoader]
private void load(TaikoInputManager taikoInputManager, OsuColour colours)
{
Debug.Assert(taikoInputManager.KeyBindingContainer != null);
keyBindingContainer = taikoInputManager.KeyBindingContainer;
// Container should handle input everywhere.
RelativeSizeAxes = Axes.Both;
const float centre_region = 0.80f;
Children = new Drawable[]
{
new Container
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.X,
Height = 350,
Y = 20,
Masking = true,
FillMode = FillMode.Fit,
Children = new Drawable[]
{
mainContent = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
leftRim = new QuarterCircle(TaikoAction.LeftRim, colours.Blue)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight,
X = -2,
},
rightRim = new QuarterCircle(TaikoAction.RightRim, colours.Blue)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight,
X = 2,
Rotation = 90,
},
leftCentre = new QuarterCircle(TaikoAction.LeftCentre, colours.Pink)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight,
X = -2,
Scale = new Vector2(centre_region),
},
rightCentre = new QuarterCircle(TaikoAction.RightCentre, colours.Pink)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight,
X = 2,
Scale = new Vector2(centre_region),
Rotation = 90,
}
}
},
}
},
};
}
protected override bool OnKeyDown(KeyDownEvent e)
{
// Hide whenever the keyboard is used.
Hide();
return false;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (!validMouse(e))
return false;
handleDown(e.Button, e.ScreenSpaceMousePosition);
return true;
}
protected override void OnMouseUp(MouseUpEvent e)
{
if (!validMouse(e))
return;
handleUp(e.Button);
base.OnMouseUp(e);
}
protected override bool OnTouchDown(TouchDownEvent e)
{
handleDown(e.Touch.Source, e.ScreenSpaceTouchDownPosition);
return true;
}
protected override void OnTouchUp(TouchUpEvent e)
{
handleUp(e.Touch.Source);
base.OnTouchUp(e);
}
private void handleDown(object source, Vector2 position)
{
Show();
TaikoAction taikoAction = getTaikoActionFromInput(position);
// Not too sure how this can happen, but let's avoid throwing.
if (trackedActions.ContainsKey(source))
return;
trackedActions.Add(source, taikoAction);
keyBindingContainer.TriggerPressed(taikoAction);
}
private void handleUp(object source)
{
keyBindingContainer.TriggerReleased(trackedActions[source]);
trackedActions.Remove(source);
}
private bool validMouse(MouseButtonEvent e) =>
leftRim.Contains(e.ScreenSpaceMouseDownPosition) || rightRim.Contains(e.ScreenSpaceMouseDownPosition);
private TaikoAction getTaikoActionFromInput(Vector2 inputPosition)
{
bool centreHit = leftCentre.Contains(inputPosition) || rightCentre.Contains(inputPosition);
bool leftSide = ToLocalSpace(inputPosition).X < DrawWidth / 2;
if (leftSide)
return centreHit ? TaikoAction.LeftCentre : TaikoAction.LeftRim;
return centreHit ? TaikoAction.RightCentre : TaikoAction.RightRim;
}
protected override void PopIn()
{
mainContent.FadeIn(500, Easing.OutQuint);
}
protected override void PopOut()
{
mainContent.FadeOut(300);
}
private class QuarterCircle : CompositeDrawable, IKeyBindingHandler<TaikoAction>
{
private readonly Circle overlay;
private readonly TaikoAction handledAction;
private readonly Circle circle;
public override bool Contains(Vector2 screenSpacePos) => circle.Contains(screenSpacePos);
public QuarterCircle(TaikoAction handledAction, Color4 colour)
{
this.handledAction = handledAction;
RelativeSizeAxes = Axes.Both;
FillMode = FillMode.Fit;
InternalChildren = new Drawable[]
{
new Container
{
Masking = true,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
circle = new Circle
{
RelativeSizeAxes = Axes.Both,
Colour = colour.Multiply(1.4f).Darken(2.8f),
Alpha = 0.8f,
Scale = new Vector2(2),
},
overlay = new Circle
{
Alpha = 0,
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Colour = colour,
Scale = new Vector2(2),
}
}
},
};
}
public bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
{
if (e.Action == handledAction)
overlay.FadeTo(1f, 80, Easing.OutQuint);
return false;
}
public void OnReleased(KeyBindingReleaseEvent<TaikoAction> e)
{
if (e.Action == handledAction)
overlay.FadeOut(1000, Easing.OutQuint);
}
}
}
}

View File

@ -12,8 +12,6 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Ranking;
using osu.Game.Skinning;
using osuTK;
@ -27,13 +25,8 @@ namespace osu.Game.Rulesets.Taiko.UI
{
private const float middle_split = 0.025f;
[Cached]
private DrumSampleTriggerSource sampleTriggerSource;
public InputDrum(HitObjectContainer hitObjectContainer)
public InputDrum()
{
sampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer);
AutoSizeAxes = Axes.X;
RelativeSizeAxes = Axes.Y;
}
@ -48,7 +41,6 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
},
sampleTriggerSource
};
}
@ -116,9 +108,6 @@ namespace osu.Game.Rulesets.Taiko.UI
private readonly Sprite centre;
private readonly Sprite centreHit;
[Resolved]
private DrumSampleTriggerSource sampleTriggerSource { get; set; }
public TaikoHalfDrum(bool flipped)
{
Masking = true;
@ -179,15 +168,11 @@ namespace osu.Game.Rulesets.Taiko.UI
{
target = centreHit;
back = centre;
sampleTriggerSource.Play(HitType.Centre);
}
else if (e.Action == RimAction)
{
target = rimHit;
back = rim;
sampleTriggerSource.Play(HitType.Rim);
}
if (target != null)

View File

@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
inputDrum = new InputDrum(HitObjectContainer)
inputDrum = new InputDrum
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
@ -164,6 +164,7 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.Both,
},
drumRollHitContainer.CreateProxy(),
new DrumSamplePlayer(HitObjectContainer),
// this is added at the end of the hierarchy to receive input before taiko objects.
// but is proxied below everything to not cover visual effects such as hit explosions.
inputDrum,

View File

@ -0,0 +1,132 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Database
{
[HeadlessTest]
public class BackgroundBeatmapProcessorTests : OsuTestScene, ILocalUserPlayInfo
{
public IBindable<bool> IsPlaying => isPlaying;
private readonly Bindable<bool> isPlaying = new Bindable<bool>();
private BeatmapSetInfo importedSet = null!;
[BackgroundDependencyLoader]
private void load(OsuGameBase osu)
{
importedSet = BeatmapImportHelper.LoadQuickOszIntoOsu(osu).GetResultSafely();
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Set not playing", () => isPlaying.Value = false);
}
[Test]
public void TestDifficultyProcessing()
{
AddAssert("Difficulty is initially set", () =>
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
});
});
AddStep("Reset difficulty", () =>
{
Realm.Write(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
foreach (var b in beatmapSetInfo.Beatmaps)
b.StarRating = -1;
});
});
AddStep("Run background processor", () =>
{
Add(new TestBackgroundBeatmapProcessor());
});
AddUntilStep("wait for difficulties repopulated", () =>
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
});
});
}
[Test]
public void TestDifficultyProcessingWhilePlaying()
{
AddAssert("Difficulty is initially set", () =>
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
});
});
AddStep("Set playing", () => isPlaying.Value = true);
AddStep("Reset difficulty", () =>
{
Realm.Write(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
foreach (var b in beatmapSetInfo.Beatmaps)
b.StarRating = -1;
});
});
AddStep("Run background processor", () =>
{
Add(new TestBackgroundBeatmapProcessor());
});
AddWaitStep("wait some", 500);
AddAssert("Difficulty still not populated", () =>
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
return beatmapSetInfo.Beatmaps.All(b => b.StarRating == -1);
});
});
AddStep("Set not playing", () => isPlaying.Value = false);
AddUntilStep("wait for difficulties repopulated", () =>
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
});
});
}
public class TestBackgroundBeatmapProcessor : BackgroundBeatmapProcessor
{
protected override int TimeToSleepDuringGameplay => 10;
}
}
}

View File

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@ -15,6 +16,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Gameplay
@ -91,6 +93,47 @@ namespace osu.Game.Tests.Gameplay
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
}
[Test]
public void TestFailScore()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects =
{
new TestHitObject(),
new TestHitObject(HitResult.LargeTickHit),
new TestHitObject(HitResult.SmallTickHit),
new TestHitObject(HitResult.SmallBonus),
new TestHitObject(),
new TestHitObject(HitResult.LargeTickHit),
new TestHitObject(HitResult.SmallTickHit),
new TestHitObject(HitResult.LargeBonus),
}
};
var scoreProcessor = new ScoreProcessor(new OsuRuleset());
scoreProcessor.ApplyBeatmap(beatmap);
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].CreateJudgement()) { Type = HitResult.Ok });
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].CreateJudgement()) { Type = HitResult.LargeTickHit });
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], beatmap.HitObjects[2].CreateJudgement()) { Type = HitResult.SmallTickMiss });
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], beatmap.HitObjects[3].CreateJudgement()) { Type = HitResult.SmallBonus });
var score = new ScoreInfo { Ruleset = new OsuRuleset().RulesetInfo };
scoreProcessor.FailScore(score);
Assert.That(score.Rank, Is.EqualTo(ScoreRank.F));
Assert.That(score.Passed, Is.False);
Assert.That(score.Statistics.Count(kvp => kvp.Value > 0), Is.EqualTo(7));
Assert.That(score.Statistics[HitResult.Ok], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.Miss], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.LargeTickHit], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.LargeTickMiss], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.SmallTickMiss], Is.EqualTo(2));
Assert.That(score.Statistics[HitResult.SmallBonus], Is.EqualTo(1));
Assert.That(score.Statistics[HitResult.IgnoreMiss], Is.EqualTo(1));
}
private class TestJudgement : Judgement
{
public override HitResult MaxResult { get; }
@ -100,5 +143,17 @@ namespace osu.Game.Tests.Gameplay
MaxResult = maxResult;
}
}
private class TestHitObject : HitObject
{
private readonly HitResult maxResult;
public TestHitObject(HitResult maxResult = HitResult.Perfect)
{
this.maxResult = maxResult;
}
public override Judgement CreateJudgement() => new TestJudgement(maxResult);
}
}
}

View File

@ -5,7 +5,6 @@
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Utils;
namespace osu.Game.Tests.Visual.Editing
{
@ -32,12 +31,12 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange);
AddStep("scale zoom", () => TimelineArea.Timeline.Zoom = 200);
AddAssert("range halved", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange / 2, 1));
AddStep("range halved", () => Assert.That(TimelineArea.Timeline.VisibleRange, Is.EqualTo(initialVisibleRange / 2).Within(1)));
AddStep("descale zoom", () => TimelineArea.Timeline.Zoom = 50);
AddAssert("range doubled", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange * 2, 1));
AddStep("range doubled", () => Assert.That(TimelineArea.Timeline.VisibleRange, Is.EqualTo(initialVisibleRange * 2).Within(1)));
AddStep("restore zoom", () => TimelineArea.Timeline.Zoom = 100);
AddAssert("range restored", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange, 1));
AddStep("range restored", () => Assert.That(TimelineArea.Timeline.VisibleRange, Is.EqualTo(initialVisibleRange).Within(1)));
}
[Test]

View File

@ -24,7 +24,6 @@ using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mods;
@ -633,7 +632,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("invoke on back button", () => multiplayerComponents.OnBackButton());
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<UserModSelectOverlay>().Single().State.Value == Visibility.Hidden);
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<RoomSubScreen>().Single().UserModsSelectOverlay.State.Value == Visibility.Hidden);
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);

View File

@ -0,0 +1,137 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets;
using osu.Game.Screens.Play;
namespace osu.Game
{
public class BackgroundBeatmapProcessor : Component
{
[Resolved]
private RulesetStore rulesetStore { get; set; } = null!;
[Resolved]
private RealmAccess realmAccess { get; set; } = null!;
[Resolved]
private BeatmapUpdater beatmapUpdater { get; set; } = null!;
[Resolved]
private IBindable<WorkingBeatmap> gameBeatmap { get; set; } = null!;
[Resolved]
private ILocalUserPlayInfo? localUserPlayInfo { get; set; }
protected virtual int TimeToSleepDuringGameplay => 30000;
protected override void LoadComplete()
{
base.LoadComplete();
Task.Run(() =>
{
Logger.Log("Beginning background beatmap processing..");
checkForOutdatedStarRatings();
processBeatmapSetsWithMissingMetrics();
}).ContinueWith(t =>
{
if (t.Exception?.InnerException is ObjectDisposedException)
{
Logger.Log("Finished background aborted during shutdown");
return;
}
Logger.Log("Finished background beatmap processing!");
});
}
/// <summary>
/// Check whether the databased difficulty calculation version matches the latest ruleset provided version.
/// If it doesn't, clear out any existing difficulties so they can be incrementally recalculated.
/// </summary>
private void checkForOutdatedStarRatings()
{
foreach (var ruleset in rulesetStore.AvailableRulesets)
{
// beatmap being passed in is arbitrary here. just needs to be non-null.
int currentVersion = ruleset.CreateInstance().CreateDifficultyCalculator(gameBeatmap.Value).Version;
if (ruleset.LastAppliedDifficultyVersion < currentVersion)
{
Logger.Log($"Resetting star ratings for {ruleset.Name} (difficulty calculation version updated from {ruleset.LastAppliedDifficultyVersion} to {currentVersion})");
int countReset = 0;
realmAccess.Write(r =>
{
foreach (var b in r.All<BeatmapInfo>())
{
if (b.Ruleset.ShortName == ruleset.ShortName)
{
b.StarRating = -1;
countReset++;
}
}
r.Find<RulesetInfo>(ruleset.ShortName).LastAppliedDifficultyVersion = currentVersion;
});
Logger.Log($"Finished resetting {countReset} beatmap sets for {ruleset.Name}");
}
}
}
private void processBeatmapSetsWithMissingMetrics()
{
HashSet<Guid> beatmapSetIds = new HashSet<Guid>();
Logger.Log("Querying for beatmap sets to reprocess...");
realmAccess.Run(r =>
{
foreach (var b in r.All<BeatmapInfo>().Where(b => b.StarRating < 0 || (b.OnlineID > 0 && b.LastOnlineUpdate == null)))
{
Debug.Assert(b.BeatmapSet != null);
beatmapSetIds.Add(b.BeatmapSet.ID);
}
});
Logger.Log($"Found {beatmapSetIds.Count} beatmap sets which require reprocessing.");
int i = 0;
foreach (var id in beatmapSetIds)
{
while (localUserPlayInfo?.IsPlaying.Value == true)
{
Logger.Log("Background processing sleeping due to active gameplay...");
Thread.Sleep(TimeToSleepDuringGameplay);
}
realmAccess.Run(r =>
{
var set = r.Find<BeatmapSetInfo>(id);
if (set != null)
{
Logger.Log($"Background processing {set} ({++i} / {beatmapSetIds.Count})");
beatmapUpdater.Process(set);
}
});
}
}
}
}

View File

@ -87,7 +87,11 @@ namespace osu.Game.Beatmaps
public string Hash { get; set; } = string.Empty;
public double StarRating { get; set; }
/// <summary>
/// Defaults to -1 (meaning not-yet-calculated).
/// Will likely be superseded with a better storage considering ruleset/mods.
/// </summary>
public double StarRating { get; set; } = -1;
[Indexed]
public string MD5Hash { get; set; } = string.Empty;

View File

@ -63,8 +63,9 @@ namespace osu.Game.Database
/// 17 2022-07-16 Added CountryCode to RealmUser.
/// 18 2022-07-19 Added OnlineMD5Hash and LastOnlineUpdate to BeatmapInfo.
/// 19 2022-07-19 Added DateSubmitted and DateRanked to BeatmapSetInfo.
/// 20 2022-07-21 Added LastAppliedDifficultyVersion to RulesetInfo, changed default value of BeatmapInfo.StarRating to -1.
/// </summary>
private const int schema_version = 19;
private const int schema_version = 20;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@ -780,6 +781,15 @@ namespace osu.Game.Database
case 14:
foreach (var beatmap in migration.NewRealm.All<BeatmapInfo>())
beatmap.UserSettings = new BeatmapUserSettings();
break;
case 20:
// As we now have versioned difficulty calculations, let's reset
// all star ratings and have `BackgroundBeatmapProcessor` recalculate them.
foreach (var beatmap in migration.NewRealm.All<BeatmapInfo>())
beatmap.StarRating = -1;
break;
}
}

View File

@ -10,9 +10,9 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics.Sprites;
@ -22,8 +22,8 @@ namespace osu.Game.Graphics.UserInterface
{
public class FPSCounter : VisibilityContainer, IHasCustomTooltip
{
private RollingCounter<double> counterUpdateFrameTime = null!;
private RollingCounter<double> counterDrawFPS = null!;
private OsuSpriteText counterUpdateFrameTime = null!;
private OsuSpriteText counterDrawFPS = null!;
private Container mainContent = null!;
@ -31,10 +31,29 @@ namespace osu.Game.Graphics.UserInterface
private Container counters = null!;
private const double min_time_between_updates = 10;
private const double spike_time_ms = 20;
private const float idle_background_alpha = 0.4f;
private readonly BindableBool showFpsDisplay = new BindableBool(true);
private double displayedFpsCount;
private double displayedFrameTime;
private bool isDisplayed;
private ScheduledDelegate? fadeOutDelegate;
private double aimDrawFPS;
private double aimUpdateFPS;
private double lastUpdate;
private ThrottledFrameClock drawClock = null!;
private ThrottledFrameClock updateClock = null!;
private ThrottledFrameClock inputClock = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
@ -44,7 +63,7 @@ namespace osu.Game.Graphics.UserInterface
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
private void load(OsuConfigManager config, GameHost gameHost)
{
InternalChildren = new Drawable[]
{
@ -77,20 +96,23 @@ namespace osu.Game.Graphics.UserInterface
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
counterUpdateFrameTime = new FrameTimeCounter
counterUpdateFrameTime = new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Margin = new MarginPadding(1),
Font = OsuFont.Default.With(fixedWidth: true, size: 16, weight: FontWeight.SemiBold),
Spacing = new Vector2(-1),
Y = -2,
},
counterDrawFPS = new FramesPerSecondCounter
counterDrawFPS = new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Margin = new MarginPadding(2),
Font = OsuFont.Default.With(fixedWidth: true, size: 13, weight: FontWeight.SemiBold),
Spacing = new Vector2(-2),
Y = 10,
Scale = new Vector2(0.8f),
}
}
},
@ -99,6 +121,10 @@ namespace osu.Game.Graphics.UserInterface
};
config.BindWith(OsuSetting.ShowFpsDisplay, showFpsDisplay);
drawClock = gameHost.DrawThread.Clock;
updateClock = gameHost.UpdateThread.Clock;
inputClock = gameHost.InputThread.Clock;
}
protected override void LoadComplete()
@ -135,13 +161,6 @@ namespace osu.Game.Graphics.UserInterface
base.OnHoverLost(e);
}
private bool isDisplayed;
private ScheduledDelegate? fadeOutDelegate;
private double aimDrawFPS;
private double aimUpdateFPS;
private void displayTemporarily()
{
if (!isDisplayed)
@ -163,9 +182,6 @@ namespace osu.Game.Graphics.UserInterface
}
}
[Resolved]
private GameHost gameHost { get; set; } = null!;
protected override void Update()
{
base.Update();
@ -176,50 +192,59 @@ namespace osu.Game.Graphics.UserInterface
// frame limiter (we want to show the FPS as it's changing, even if it isn't an outlier).
bool aimRatesChanged = updateAimFPS();
// TODO: this is wrong (elapsed clock time, not actual run time).
double newUpdateFrameTime = gameHost.UpdateThread.Clock.ElapsedFrameTime;
double newDrawFrameTime = gameHost.DrawThread.Clock.ElapsedFrameTime;
double newDrawFps = gameHost.DrawThread.Clock.FramesPerSecond;
const double spike_time_ms = 20;
bool hasUpdateSpike = counterUpdateFrameTime.Current.Value < spike_time_ms && newUpdateFrameTime > spike_time_ms;
bool hasUpdateSpike = displayedFrameTime < spike_time_ms && updateClock.ElapsedFrameTime > spike_time_ms;
// use elapsed frame time rather then FramesPerSecond to better catch stutter frames.
bool hasDrawSpike = counterDrawFPS.Current.Value > (1000 / spike_time_ms) && newDrawFrameTime > spike_time_ms;
bool hasDrawSpike = displayedFpsCount > (1000 / spike_time_ms) && drawClock.ElapsedFrameTime > spike_time_ms;
// If the frame time spikes up, make sure it shows immediately on the counter.
if (hasUpdateSpike)
counterUpdateFrameTime.SetCountWithoutRolling(newUpdateFrameTime);
else
counterUpdateFrameTime.Current.Value = newUpdateFrameTime;
// note that we use an elapsed time here of 1 intentionally.
// this weights all updates equally. if we passed in the elapsed time, longer frames would be weighted incorrectly lower.
displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, updateClock.ElapsedFrameTime, hasUpdateSpike ? 0 : 100, 1);
if (hasDrawSpike)
// show spike time using raw elapsed value, to account for `FramesPerSecond` being so averaged spike frames don't show.
counterDrawFPS.SetCountWithoutRolling(1000 / newDrawFrameTime);
displayedFpsCount = 1000 / drawClock.ElapsedFrameTime;
else
counterDrawFPS.Current.Value = newDrawFps;
displayedFpsCount = Interpolation.DampContinuously(displayedFpsCount, drawClock.FramesPerSecond, 100, Time.Elapsed);
counterDrawFPS.Colour = getColour(counterDrawFPS.DisplayedCount / aimDrawFPS);
if (Time.Current - lastUpdate > min_time_between_updates)
{
updateFpsDisplay();
updateFrameTimeDisplay();
double displayedUpdateFPS = 1000 / counterUpdateFrameTime.DisplayedCount;
counterUpdateFrameTime.Colour = getColour(displayedUpdateFPS / aimUpdateFPS);
lastUpdate = Time.Current;
}
bool hasSignificantChanges = aimRatesChanged
|| hasDrawSpike
|| hasUpdateSpike
|| counterDrawFPS.DisplayedCount < aimDrawFPS * 0.8
|| displayedUpdateFPS < aimUpdateFPS * 0.8;
|| displayedFpsCount < aimDrawFPS * 0.8
|| 1000 / displayedFrameTime < aimUpdateFPS * 0.8;
if (hasSignificantChanges)
displayTemporarily();
}
private void updateFpsDisplay()
{
counterDrawFPS.Colour = getColour(displayedFpsCount / aimDrawFPS);
counterDrawFPS.Text = $"{displayedFpsCount:#,0}fps";
}
private void updateFrameTimeDisplay()
{
counterUpdateFrameTime.Text = displayedFrameTime < 5
? $"{displayedFrameTime:N1}ms"
: $"{displayedFrameTime:N0}ms";
counterUpdateFrameTime.Colour = getColour((1000 / displayedFrameTime) / aimUpdateFPS);
}
private bool updateAimFPS()
{
if (gameHost.UpdateThread.Clock.Throttling)
if (updateClock.Throttling)
{
double newAimDrawFPS = gameHost.DrawThread.Clock.MaximumUpdateHz;
double newAimUpdateFPS = gameHost.UpdateThread.Clock.MaximumUpdateHz;
double newAimDrawFPS = drawClock.MaximumUpdateHz;
double newAimUpdateFPS = updateClock.MaximumUpdateHz;
if (aimDrawFPS != newAimDrawFPS || aimUpdateFPS != newAimUpdateFPS)
{
@ -230,7 +255,7 @@ namespace osu.Game.Graphics.UserInterface
}
else
{
double newAimFPS = gameHost.InputThread.Clock.MaximumUpdateHz;
double newAimFPS = inputClock.MaximumUpdateHz;
if (aimDrawFPS != newAimFPS || aimUpdateFPS != newAimFPS)
{
@ -253,50 +278,5 @@ namespace osu.Game.Graphics.UserInterface
public ITooltip GetCustomTooltip() => new FPSCounterTooltip();
public object TooltipContent => this;
public class FramesPerSecondCounter : RollingCounter<double>
{
protected override double RollingDuration => 1000;
protected override OsuSpriteText CreateSpriteText()
{
return new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Font = OsuFont.Default.With(fixedWidth: true, size: 16, weight: FontWeight.SemiBold),
Spacing = new Vector2(-2),
};
}
protected override LocalisableString FormatCount(double count)
{
return $"{count:#,0}fps";
}
}
public class FrameTimeCounter : RollingCounter<double>
{
protected override double RollingDuration => 1000;
protected override OsuSpriteText CreateSpriteText()
{
return new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Font = OsuFont.Default.With(fixedWidth: true, size: 16, weight: FontWeight.SemiBold),
Spacing = new Vector2(-1),
};
}
protected override LocalisableString FormatCount(double count)
{
if (count < 1)
return $"{count:N1}ms";
return $"{count:N0}ms";
}
}
}
}

View File

@ -34,6 +34,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString RestartTrack => new TranslatableString(getKey(@"restart_track"), @"Restart track");
/// <summary>
/// "Beatmap saved"
/// </summary>
public static LocalisableString BeatmapSaved => new TranslatableString(getKey(@"beatmap_saved"), @"Beatmap saved");
/// <summary>
/// "Skin saved"
/// </summary>
public static LocalisableString SkinSaved => new TranslatableString(getKey(@"skin_saved"), @"Skin saved");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -904,6 +904,8 @@ namespace osu.Game
loadComponentSingleFile(CreateHighPerformanceSession(), Add);
loadComponentSingleFile(new BackgroundBeatmapProcessor(), Add);
chatOverlay.State.BindValueChanged(_ => updateChatPollRate());
// Multiplayer modes need to increase poll rate temporarily.
API.Activity.BindValueChanged(_ => updateChatPollRate(), true);

View File

@ -280,8 +280,7 @@ namespace osu.Game
AddInternal(difficultyCache);
// TODO: OsuGame or OsuGameBase?
beatmapUpdater = new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage);
dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage));
dependencies.CacheAs(spectatorClient = new OnlineSpectatorClient(endpoints));
dependencies.CacheAs(multiplayerClient = new OnlineMultiplayerClient(endpoints));
dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));

View File

@ -34,6 +34,11 @@ namespace osu.Game.Rulesets.Difficulty
private readonly IRulesetInfo ruleset;
private readonly IWorkingBeatmap beatmap;
/// <summary>
/// A yymmdd version which is used to discern when reprocessing is required.
/// </summary>
public virtual int Version => 0;
protected DifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
{
this.ruleset = ruleset;

View File

@ -4,6 +4,7 @@
using System;
using JetBrains.Annotations;
using osu.Framework.Testing;
using osu.Game.Rulesets.Difficulty;
using Realms;
namespace osu.Game.Rulesets
@ -22,6 +23,11 @@ namespace osu.Game.Rulesets
public string InstantiationInfo { get; set; } = string.Empty;
/// <summary>
/// Stores the last applied <see cref="DifficultyCalculator.Version"/>
/// </summary>
public int LastAppliedDifficultyVersion { get; set; }
public RulesetInfo(string shortName, string name, string instantiationInfo, int onlineID)
{
ShortName = shortName;
@ -86,7 +92,8 @@ namespace osu.Game.Rulesets
Name = Name,
ShortName = ShortName,
InstantiationInfo = InstantiationInfo,
Available = Available
Available = Available,
LastAppliedDifficultyVersion = LastAppliedDifficultyVersion,
};
public Ruleset CreateInstance()

View File

@ -126,6 +126,9 @@ namespace osu.Game.Rulesets.Scoring
private bool beatmapApplied;
private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>();
private Dictionary<HitResult, int>? maximumResultCounts;
private readonly List<HitEvent> hitEvents = new List<HitEvent>();
private HitObject? lastHitObject;
@ -410,12 +413,16 @@ namespace osu.Game.Rulesets.Scoring
{
base.Reset(storeResults);
scoreResultCounts.Clear();
hitEvents.Clear();
lastHitObject = null;
if (storeResults)
{
maximumScoringValues = currentScoringValues;
maximumResultCounts = new Dictionary<HitResult, int>(scoreResultCounts);
}
scoreResultCounts.Clear();
currentScoringValues = default;
currentMaximumScoringValues = default;
@ -423,6 +430,7 @@ namespace osu.Game.Rulesets.Scoring
TotalScore.Value = 0;
Accuracy.Value = 1;
Combo.Value = 0;
Rank.Disabled = false;
Rank.Value = ScoreRank.X;
HighestCombo.Value = 0;
}
@ -445,6 +453,36 @@ namespace osu.Game.Rulesets.Scoring
score.TotalScore = (long)Math.Round(ComputeFinalScore(ScoringMode.Standardised, score));
}
/// <summary>
/// Populates the given score with remaining statistics as "missed" and marks it with <see cref="ScoreRank.F"/> rank.
/// </summary>
public void FailScore(ScoreInfo score)
{
if (Rank.Value == ScoreRank.F)
return;
score.Passed = false;
Rank.Value = ScoreRank.F;
Debug.Assert(maximumResultCounts != null);
if (maximumResultCounts.TryGetValue(HitResult.LargeTickHit, out int maximumLargeTick))
scoreResultCounts[HitResult.LargeTickMiss] = maximumLargeTick - scoreResultCounts.GetValueOrDefault(HitResult.LargeTickHit);
if (maximumResultCounts.TryGetValue(HitResult.SmallTickHit, out int maximumSmallTick))
scoreResultCounts[HitResult.SmallTickMiss] = maximumSmallTick - scoreResultCounts.GetValueOrDefault(HitResult.SmallTickHit);
int maximumBonusOrIgnore = maximumResultCounts.Where(kvp => kvp.Key.IsBonus() || kvp.Key == HitResult.IgnoreHit).Sum(kvp => kvp.Value);
int currentBonusOrIgnore = scoreResultCounts.Where(kvp => kvp.Key.IsBonus() || kvp.Key == HitResult.IgnoreHit).Sum(kvp => kvp.Value);
scoreResultCounts[HitResult.IgnoreMiss] = maximumBonusOrIgnore - currentBonusOrIgnore;
int maximumBasic = maximumResultCounts.SingleOrDefault(kvp => kvp.Key.IsBasic()).Value;
int currentBasic = scoreResultCounts.Where(kvp => kvp.Key.IsBasic() && kvp.Key != HitResult.Miss).Sum(kvp => kvp.Value);
scoreResultCounts[HitResult.Miss] = maximumBasic - currentBasic;
PopulateScore(score);
}
public override void ResetFromReplayFrame(ReplayFrame frame)
{
base.ResetFromReplayFrame(frame);

View File

@ -41,6 +41,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved]
private EditorClock editorClock { get; set; }
[Resolved]
private EditorBeatmap editorBeatmap { get; set; }
/// <summary>
/// The timeline's scroll position in the last frame.
/// </summary>
@ -68,8 +71,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
/// </summary>
private float defaultTimelineZoom;
private readonly Bindable<double> timelineZoomScale = new BindableDouble(1.0);
public Timeline(Drawable userContent)
{
this.userContent = userContent;
@ -93,7 +94,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private Bindable<float> waveformOpacity;
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, EditorBeatmap editorBeatmap, OsuColour colours, OsuConfigManager config)
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours, OsuConfigManager config)
{
CentreMarker centreMarker;
@ -154,12 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}, true);
timelineZoomScale.Value = editorBeatmap.BeatmapInfo.TimelineZoom;
timelineZoomScale.BindValueChanged(scale =>
{
Zoom = (float)(defaultTimelineZoom * scale.NewValue);
editorBeatmap.BeatmapInfo.TimelineZoom = scale.NewValue;
}, true);
Zoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom);
}
protected override void LoadComplete()
@ -221,7 +217,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override void OnZoomChanged()
{
base.OnZoomChanged();
timelineZoomScale.Value = Zoom / defaultTimelineZoom;
editorBeatmap.BeatmapInfo.TimelineZoom = Zoom / defaultTimelineZoom;
}
protected override void UpdateAfterChildren()

View File

@ -118,7 +118,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.Y,
Height = 0.5f,
Icon = FontAwesome.Solid.SearchPlus,
Action = () => changeZoom(1)
Action = () => Timeline.AdjustZoomRelatively(1)
},
new TimelineButton
{
@ -127,7 +127,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.Y,
Height = 0.5f,
Icon = FontAwesome.Solid.SearchMinus,
Action = () => changeZoom(-1)
Action = () => Timeline.AdjustZoomRelatively(-1)
},
}
}
@ -153,7 +153,5 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Timeline.ControlPointsVisible.BindTo(controlPointsCheckbox.Current);
Timeline.TicksVisible.BindTo(ticksCheckbox.Current);
}
private void changeZoom(float change) => Timeline.Zoom += change;
}
}

View File

@ -127,7 +127,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (e.AltPressed)
{
// zoom when holding alt.
setZoomTarget(zoomTarget + e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X);
AdjustZoomRelatively(e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X);
return true;
}
@ -145,12 +145,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
zoomedContentWidthCache.Validate();
}
public void AdjustZoomRelatively(float change, float? focusPoint = null)
{
const float zoom_change_sensitivity = 0.02f;
setZoomTarget(zoomTarget + change * (MaxZoom - minZoom) * zoom_change_sensitivity, focusPoint);
}
private float zoomTarget = 1;
private void setZoomTarget(float newZoom, float focusPoint)
private void setZoomTarget(float newZoom, float? focusPoint = null)
{
zoomTarget = Math.Clamp(newZoom, MinZoom, MaxZoom);
transformZoomTo(zoomTarget, focusPoint, ZoomDuration, ZoomEasing);
focusPoint ??= zoomedContent.ToLocalSpace(ToScreenSpace(new Vector2(DrawWidth / 2, 0))).X;
transformZoomTo(zoomTarget, focusPoint.Value, ZoomDuration, ZoomEasing);
OnZoomChanged();
}

View File

@ -18,6 +18,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
@ -31,10 +32,11 @@ using osu.Game.Database;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Overlays.OSD;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
@ -50,6 +52,7 @@ using osu.Game.Screens.Play;
using osu.Game.Users;
using osuTK.Graphics;
using osuTK.Input;
using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
namespace osu.Game.Screens.Edit
{
@ -169,6 +172,9 @@ namespace osu.Game.Screens.Edit
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[Resolved(canBeNull: true)]
private OnScreenDisplay onScreenDisplay { get; set; }
public Editor(EditorLoader loader = null)
{
this.loader = loader;
@ -405,6 +411,7 @@ namespace osu.Game.Screens.Edit
// no longer new after first user-triggered save.
isNewBeatmap = false;
updateLastSavedHash();
onScreenDisplay?.Display(new BeatmapEditorToast(ToastStrings.BeatmapSaved, editorBeatmap.BeatmapInfo.GetDisplayTitle()));
return true;
}
@ -934,5 +941,13 @@ namespace osu.Game.Screens.Edit
ControlPointInfo IBeatSyncProvider.ControlPoints => editorBeatmap.ControlPointInfo;
IClock IBeatSyncProvider.Clock => clock;
ChannelAmplitudes? IBeatSyncProvider.Amplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : null;
private class BeatmapEditorToast : Toast
{
public BeatmapEditorToast(LocalisableString value, string beatmapDisplayName)
: base(InputSettingsStrings.EditorSection, value, beatmapDisplayName)
{
}
}
}
}

View File

@ -267,12 +267,7 @@ namespace osu.Game.Screens.Play
},
FailOverlay = new FailOverlay
{
SaveReplay = () =>
{
Score.ScoreInfo.Passed = false;
Score.ScoreInfo.Rank = ScoreRank.F;
return prepareAndImportScore();
},
SaveReplay = prepareAndImportScore,
OnRetry = Restart,
OnQuit = () => PerformExit(true),
},
@ -831,7 +826,6 @@ namespace osu.Game.Screens.Play
return false;
GameplayState.HasFailed = true;
Score.ScoreInfo.Passed = false;
updateGameplayState();
@ -849,9 +843,16 @@ namespace osu.Game.Screens.Play
return true;
}
// Called back when the transform finishes
/// <summary>
/// Invoked when the fail animation has finished.
/// </summary>
private void onFailComplete()
{
// fail completion is a good point to mark a score as failed,
// since the last judgement that caused the fail only applies to score processor after onFail.
// todo: this should probably be handled better.
ScoreProcessor.FailScore(Score.ScoreInfo);
GameplayClockContainer.Stop();
FailOverlay.Retries = RestartCount;
@ -1028,10 +1029,7 @@ namespace osu.Game.Screens.Play
// if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap.
if (prepareScoreForDisplayTask == null)
{
Score.ScoreInfo.Passed = false;
Score.ScoreInfo.Rank = ScoreRank.F;
}
ScoreProcessor.FailScore(Score.ScoreInfo);
// EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous.
// To resolve test failures, forcefully end playing synchronously when this screen exits.

View File

@ -115,42 +115,53 @@ namespace osu.Game.Screens.Select
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
},
new FillFlowContainer
new GridContainer
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Direction = FillDirection.Horizontal,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(OsuTabControl<SortMode>.HORIZONTAL_SPACING, 0),
Children = new Drawable[]
ColumnDimensions = new[]
{
new OsuTabControlCheckbox
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, OsuTabControl<SortMode>.HORIZONTAL_SPACING),
new Dimension(),
new Dimension(GridSizeMode.Absolute, OsuTabControl<SortMode>.HORIZONTAL_SPACING),
new Dimension(GridSizeMode.AutoSize),
},
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
Content = new[]
{
new[]
{
Text = "Show converted",
Current = config.GetBindable<bool>(OsuSetting.ShowConvertedBeatmaps),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
sortTabs = new OsuTabControl<SortMode>
{
RelativeSizeAxes = Axes.X,
Width = 0.5f,
Height = 24,
AutoSort = true,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
AccentColour = colours.GreenLight,
Current = { BindTarget = sortMode }
},
new OsuSpriteText
{
Text = SortStrings.Default,
Font = OsuFont.GetFont(size: 14),
Margin = new MarginPadding(5),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
new OsuSpriteText
{
Text = SortStrings.Default,
Font = OsuFont.GetFont(size: 14),
Margin = new MarginPadding(5),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
Empty(),
sortTabs = new OsuTabControl<SortMode>
{
RelativeSizeAxes = Axes.X,
Height = 24,
AutoSort = true,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
AccentColour = colours.GreenLight,
Current = { BindTarget = sortMode }
},
Empty(),
new OsuTabControlCheckbox
{
Text = "Show converted",
Current = config.GetBindable<bool>(OsuSetting.ShowConvertedBeatmaps),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
}
}
},
}

View File

@ -13,21 +13,26 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Overlays.OSD;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Edit.Components.Menus;
namespace osu.Game.Skinning.Editor
{
[Cached(typeof(SkinEditor))]
public class SkinEditor : VisibilityContainer, ICanAcceptFiles
public class SkinEditor : VisibilityContainer, ICanAcceptFiles, IKeyBindingHandler<PlatformAction>
{
public const double TRANSITION_DURATION = 500;
@ -68,6 +73,9 @@ namespace osu.Game.Skinning.Editor
private EditorSidebar componentsSidebar;
private EditorSidebar settingsSidebar;
[Resolved(canBeNull: true)]
private OnScreenDisplay onScreenDisplay { get; set; }
public SkinEditor()
{
}
@ -199,6 +207,25 @@ namespace osu.Game.Skinning.Editor
SelectedComponents.BindCollectionChanged((_, _) => Scheduler.AddOnce(populateSettings), true);
}
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
{
switch (e.Action)
{
case PlatformAction.Save:
if (e.Repeat)
return false;
Save();
return true;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)
{
}
public void UpdateTargetScreen(Drawable targetScreen)
{
this.targetScreen = targetScreen;
@ -316,6 +343,7 @@ namespace osu.Game.Skinning.Editor
currentSkin.Value.UpdateDrawableTarget(t);
skins.Save(skins.CurrentSkin.Value);
onScreenDisplay?.Display(new SkinEditorToast(ToastStrings.SkinSaved, currentSkin.Value.SkinInfo.ToString()));
}
protected override bool OnHover(HoverEvent e) => true;
@ -395,5 +423,13 @@ namespace osu.Game.Skinning.Editor
game?.UnregisterImportHandler(this);
}
private class SkinEditorToast : Toast
{
public SkinEditorToast(LocalisableString value, string skinDisplayName)
: base(SkinSettingsStrings.SkinLayoutEditor, value, skinDisplayName)
{
}
}
}
}