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

Merge branch 'master' into framework-clipboard

This commit is contained in:
Dean Herbert 2023-07-14 03:32:37 +09:00 committed by GitHub
commit ded7ec3aa6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 888 additions and 266 deletions

View File

@ -21,7 +21,7 @@
]
},
"ppy.localisationanalyser.tools": {
"version": "2022.809.0",
"version": "2023.712.0",
"commands": [
"localisation"
]

View File

@ -5,6 +5,7 @@ using System;
using NUnit.Framework;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Catch.Tests
@ -24,7 +25,8 @@ namespace osu.Game.Rulesets.Catch.Tests
new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } },
new object[] { LegacyMods.Flashlight, new[] { typeof(CatchModFlashlight) } },
new object[] { LegacyMods.Autoplay, new[] { typeof(CatchModAutoplay) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } }
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } },
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
};
[TestCaseSource(nameof(catch_mod_mapping))]

View File

@ -26,6 +26,8 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
@ -91,6 +93,9 @@ namespace osu.Game.Rulesets.Catch
if (mods.HasFlagFast(LegacyMods.Relax))
yield return new CatchModRelax();
if (mods.HasFlagFast(LegacyMods.ScoreV2))
yield return new ModScoreV2();
}
public override IEnumerable<Mod> GetModsFor(ModType type)
@ -140,6 +145,12 @@ namespace osu.Game.Rulesets.Catch
new CatchModNoScope(),
};
case ModType.System:
return new Mod[]
{
new ModScoreV2(),
};
default:
return Array.Empty<Mod>();
}
@ -209,5 +220,17 @@ namespace osu.Game.Rulesets.Catch
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
return new[]
{
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}),
};
}
}
}

View File

@ -6,7 +6,6 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
@ -24,7 +23,5 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
: base(new THitObject())
{
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
}
}

View File

@ -5,6 +5,7 @@ using System;
using NUnit.Framework;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests
@ -36,7 +37,8 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { LegacyMods.Key3, new[] { typeof(ManiaModKey3) } },
new object[] { LegacyMods.Key2, new[] { typeof(ManiaModKey2) } },
new object[] { LegacyMods.Mirror, new[] { typeof(ManiaModMirror) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } }
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } },
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
};
[TestCaseSource(nameof(mania_mod_mapping))]

View File

@ -0,0 +1,147 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
public partial class TestSceneMaximumScore : RateAdjustedBeatmapTestScene
{
private ScoreAccessibleReplayPlayer currentPlayer = null!;
private List<JudgementResult> judgementResults = new List<JudgementResult>();
[Test]
public void TestSimultaneousTickAndNote()
{
performTest(
new List<ManiaHitObject>
{
new HoldNote
{
StartTime = 1000,
Duration = 2000,
Column = 0,
},
new Note
{
StartTime = 2000,
Column = 1
}
},
new List<ReplayFrame>
{
new ManiaReplayFrame(1000, ManiaAction.Key1),
new ManiaReplayFrame(2000, ManiaAction.Key1, ManiaAction.Key2),
new ManiaReplayFrame(2001, ManiaAction.Key1),
new ManiaReplayFrame(3000)
});
AddAssert("all objects perfectly judged",
() => judgementResults.Select(result => result.Type),
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
}
[Test]
public void TestSimultaneousLongNotes()
{
performTest(
new List<ManiaHitObject>
{
new HoldNote
{
StartTime = 1000,
Duration = 2000,
Column = 0,
},
new HoldNote
{
StartTime = 2000,
Duration = 2000,
Column = 1
}
},
new List<ReplayFrame>
{
new ManiaReplayFrame(1000, ManiaAction.Key1),
new ManiaReplayFrame(2000, ManiaAction.Key1, ManiaAction.Key2),
new ManiaReplayFrame(3000, ManiaAction.Key2),
new ManiaReplayFrame(4000)
});
AddAssert("all objects perfectly judged",
() => judgementResults.Select(result => result.Type),
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
}
private void performTest(List<ManiaHitObject> hitObjects, List<ReplayFrame> frames)
{
var beatmap = new Beatmap<ManiaHitObject>
{
HitObjects = hitObjects,
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new ManiaRuleset().RulesetInfo
},
};
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
AddStep("load player", () =>
{
Beatmap.Value = CreateWorkingBeatmap(beatmap);
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
{
p.ScoreProcessor.NewJudgement += result =>
{
if (currentPlayer == p) judgementResults.Add(result);
};
};
LoadScreen(currentPlayer = p);
judgementResults = new List<JudgementResult>();
});
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
}
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleReplayPlayer(Score score)
: base(score, new PlayerConfiguration
{
AllowPause = false,
ShowResults = false,
})
{
}
}
}
}

View File

@ -157,6 +157,9 @@ namespace osu.Game.Rulesets.Mania
if (mods.HasFlagFast(LegacyMods.Mirror))
yield return new ManiaModMirror();
if (mods.HasFlagFast(LegacyMods.ScoreV2))
yield return new ModScoreV2();
}
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
@ -285,6 +288,12 @@ namespace osu.Game.Rulesets.Mania
new ModAdaptiveSpeed()
};
case ModType.System:
return new Mod[]
{
new ModScoreV2(),
};
default:
return Array.Empty<Mod>();
}
@ -403,7 +412,7 @@ namespace osu.Game.Rulesets.Mania
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
new StatisticItem("Statistics", () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new AverageHitError(score.HitEvents),
new UnstableRate(score.HitEvents)

View File

@ -2,7 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Scoring
@ -16,6 +21,9 @@ namespace osu.Game.Rulesets.Mania.Scoring
{
}
protected override IEnumerable<HitObject> EnumerateHitObjects(IBeatmap beatmap)
=> base.EnumerateHitObjects(beatmap).OrderBy(ho => (ManiaHitObject)ho, JudgementOrderComparer.DEFAULT);
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{
return 10000 * comboProgress
@ -25,5 +33,27 @@ namespace osu.Game.Rulesets.Mania.Scoring
protected override double GetComboScoreChange(JudgementResult result)
=> Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base));
private class JudgementOrderComparer : IComparer<ManiaHitObject>
{
public static readonly JudgementOrderComparer DEFAULT = new JudgementOrderComparer();
public int Compare(ManiaHitObject? x, ManiaHitObject? y)
{
if (ReferenceEquals(x, y)) return 0;
if (ReferenceEquals(x, null)) return -1;
if (ReferenceEquals(y, null)) return 1;
int result = x.GetEndTime().CompareTo(y.GetEndTime());
if (result != 0)
return result;
// due to the way input is handled in mania, notes take precedence over ticks in judging order.
if (x is Note && y is not Note) return -1;
if (x is not Note && y is Note) return 1;
return x.Column.CompareTo(y.Column);
}
}
}
}

View File

@ -61,6 +61,20 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointType(0, PathType.Linear);
}
[Test]
public void TestPlaceWithMouseMovementOutsidePlayfield()
{
addMovementStep(new Vector2(200));
addClickStep(MouseButton.Left);
AddStep("move mouse out of screen", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + Vector2.One));
addClickStep(MouseButton.Right);
assertPlaced(true);
assertControlPointCount(2);
assertControlPointType(0, PathType.Linear);
}
[Test]
public void TestPlaceNormalControlPoint()
{

View File

@ -4,6 +4,7 @@
using System;
using NUnit.Framework;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Beatmaps;
@ -28,7 +29,8 @@ namespace osu.Game.Rulesets.Osu.Tests
new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } },
new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } },
new object[] { LegacyMods.Target, new[] { typeof(OsuModTargetPractice) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } }
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } },
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
};
[TestCaseSource(nameof(osu_mod_mapping))]

View File

@ -214,17 +214,24 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
? currSlider.Position + currSlider.Path.PositionAt(1)
: currHitObject.Position;
// Note the use of `StartTime` in the code below doesn't match stable's use of `EndTime`.
// This is because in the stable implementation, `UpdateCalculations` is not called on the inner-loop hitobject (j)
// and therefore it does not have a correct `EndTime`, but instead the default of `EndTime = StartTime`.
//
// Effects of this can be seen on https://osu.ppy.sh/beatmapsets/243#osu/1146 at sliders around 86647 ms, where
// if we use `EndTime` here it would result in unexpected stacking.
if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.Position) < stack_distance)
{
currHitObject.StackHeight++;
startTime = beatmap.HitObjects[j].GetEndTime();
startTime = beatmap.HitObjects[j].StartTime;
}
else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, position2) < stack_distance)
{
// Case for sliders - bump notes down and right, rather than up and left.
sliderStack++;
beatmap.HitObjects[j].StackHeight -= sliderStack;
startTime = beatmap.HitObjects[j].GetEndTime();
startTime = beatmap.HitObjects[j].StartTime;
}
}
}

View File

@ -113,6 +113,9 @@ namespace osu.Game.Rulesets.Osu
if (mods.HasFlagFast(LegacyMods.TouchDevice))
yield return new OsuModTouchDevice();
if (mods.HasFlagFast(LegacyMods.ScoreV2))
yield return new ModScoreV2();
}
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
@ -212,6 +215,7 @@ namespace osu.Game.Rulesets.Osu
return new Mod[]
{
new OsuModTouchDevice(),
new ModScoreV2(),
};
default:
@ -315,7 +319,7 @@ namespace osu.Game.Rulesets.Osu
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
new StatisticItem("Statistics", () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new AverageHitError(timedHitEvents),
new UnstableRate(timedHitEvents)

View File

@ -114,5 +114,75 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
AddAssert("all tick offsets are 0", () => JudgementResults.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0));
}
/// <summary>
/// Ensure input is correctly sent to subsequent hits if a swell is fully completed.
/// </summary>
[Test]
public void TestHitSwellThenHitHit()
{
const double swell_time = 1000;
const double hit_time = 1150;
Swell swell = new Swell
{
StartTime = swell_time,
Duration = 100,
RequiredHits = 1
};
Hit hit = new Hit
{
StartTime = hit_time
};
List<ReplayFrame> frames = new List<ReplayFrame>
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(swell_time, TaikoAction.LeftRim),
new TaikoReplayFrame(hit_time, TaikoAction.RightCentre),
};
PerformTest(frames, CreateBeatmap(swell, hit));
AssertJudgementCount(3);
AssertResult<SwellTick>(0, HitResult.IgnoreHit);
AssertResult<Swell>(0, HitResult.LargeBonus);
AssertResult<Hit>(0, HitResult.Great);
}
[Test]
public void TestMissSwellThenHitHit()
{
const double swell_time = 1000;
const double hit_time = 1150;
Swell swell = new Swell
{
StartTime = swell_time,
Duration = 100,
RequiredHits = 1
};
Hit hit = new Hit
{
StartTime = hit_time
};
List<ReplayFrame> frames = new List<ReplayFrame>
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(hit_time, TaikoAction.RightCentre),
};
PerformTest(frames, CreateBeatmap(swell, hit));
AssertJudgementCount(3);
AssertResult<SwellTick>(0, HitResult.IgnoreMiss);
AssertResult<Swell>(0, HitResult.IgnoreMiss);
AssertResult<Hit>(0, HitResult.Great);
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using NUnit.Framework;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Tests.Beatmaps;
@ -25,7 +26,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
new object[] { LegacyMods.Flashlight, new[] { typeof(TaikoModFlashlight) } },
new object[] { LegacyMods.Autoplay, new[] { typeof(TaikoModAutoplay) } },
new object[] { LegacyMods.Random, new[] { typeof(TaikoModRandom) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } }
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } },
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
};
[TestCaseSource(nameof(taiko_mod_mapping))]

View File

@ -276,6 +276,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (Time.Current < HitObject.StartTime)
return false;
if (AllJudged)
return false;
bool isCentre = e.Action == TaikoAction.LeftCentre || e.Action == TaikoAction.RightCentre;
// Ensure alternating centre and rim hits

View File

@ -116,6 +116,9 @@ namespace osu.Game.Rulesets.Taiko
if (mods.HasFlagFast(LegacyMods.Random))
yield return new TaikoModRandom();
if (mods.HasFlagFast(LegacyMods.ScoreV2))
yield return new ModScoreV2();
}
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
@ -176,6 +179,12 @@ namespace osu.Game.Rulesets.Taiko
new ModAdaptiveSpeed()
};
case ModType.System:
return new Mod[]
{
new ModScoreV2(),
};
default:
return Array.Empty<Mod>();
}
@ -247,7 +256,7 @@ namespace osu.Game.Rulesets.Taiko
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
new StatisticItem("Statistics", () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new AverageHitError(timedHitEvents),
new UnstableRate(timedHitEvents)

View File

@ -0,0 +1,85 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks
{
public class CheckDrainTimeTest
{
private CheckDrainTime check = null!;
[SetUp]
public void Setup()
{
check = new CheckDrainTime();
}
[Test]
public void TestDrainTimeShort()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects =
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 29_999 }
}
};
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckDrainTime.IssueTemplateTooShort);
}
[Test]
public void TestDrainTimeBreak()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects =
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 40_000 }
},
Breaks = new List<BreakPeriod>
{
new BreakPeriod(10_000, 21_000)
}
};
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckDrainTime.IssueTemplateTooShort);
}
[Test]
public void TestDrainTimeCorrect()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i <= 30; ++i)
hitObjects.Add(new HitCircle { StartTime = 1000 * i });
var beatmap = new Beatmap<HitObject> { HitObjects = hitObjects };
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
var issues = check.Run(context).ToList();
Assert.That(issues, Is.Empty);
}
}
}

View File

@ -69,6 +69,35 @@ namespace osu.Game.Tests.Visual.Ranking
}));
}
private int onlineScoreID = 1;
[TestCase(1, ScoreRank.X)]
[TestCase(0.9999, ScoreRank.S)]
[TestCase(0.975, ScoreRank.S)]
[TestCase(0.925, ScoreRank.A)]
[TestCase(0.85, ScoreRank.B)]
[TestCase(0.75, ScoreRank.C)]
[TestCase(0.5, ScoreRank.D)]
[TestCase(0.2, ScoreRank.D)]
public void TestResultsWithPlayer(double accuracy, ScoreRank rank)
{
TestResultsScreen screen = null;
loadResultsScreen(() =>
{
var score = TestResources.CreateTestScoreInfo();
score.OnlineID = onlineScoreID++;
score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents();
score.Accuracy = accuracy;
score.Rank = rank;
return screen = createResultsScreen(score);
});
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay present", () => screen.RetryOverlay != null);
}
[Test]
public void TestResultsWithoutPlayer()
{
@ -82,34 +111,14 @@ namespace osu.Game.Tests.Visual.Ranking
RelativeSizeAxes = Axes.Both
};
stack.Push(screen = createResultsScreen());
var score = TestResources.CreateTestScoreInfo();
stack.Push(screen = createResultsScreen(score));
});
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay not present", () => screen.RetryOverlay == null);
}
[TestCase(0.2, ScoreRank.D)]
[TestCase(0.5, ScoreRank.D)]
[TestCase(0.75, ScoreRank.C)]
[TestCase(0.85, ScoreRank.B)]
[TestCase(0.925, ScoreRank.A)]
[TestCase(0.975, ScoreRank.S)]
[TestCase(0.9999, ScoreRank.S)]
[TestCase(1, ScoreRank.X)]
public void TestResultsWithPlayer(double accuracy, ScoreRank rank)
{
TestResultsScreen screen = null;
var score = TestResources.CreateTestScoreInfo();
score.Accuracy = accuracy;
score.Rank = rank;
loadResultsScreen(() => screen = createResultsScreen(score));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay present", () => screen.RetryOverlay != null);
}
[Test]
public void TestResultsForUnranked()
{
@ -328,13 +337,14 @@ namespace osu.Game.Tests.Visual.Ranking
}
}
private partial class TestResultsScreen : ResultsScreen
private partial class TestResultsScreen : SoloResultsScreen
{
public HotkeyRetryOverlay RetryOverlay;
public TestResultsScreen(ScoreInfo score)
: base(score, true)
{
ShowUserStatistics = true;
}
protected override void LoadComplete()

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Solo;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
@ -23,6 +24,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Tests.Visual.Ranking
@ -30,19 +32,20 @@ namespace osu.Game.Tests.Visual.Ranking
public partial class TestSceneStatisticsPanel : OsuTestScene
{
[Test]
public void TestScoreWithTimeStatistics()
public void TestScoreWithPositionStatistics()
{
var score = TestResources.CreateTestScoreInfo();
score.HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents();
score.OnlineID = 1234;
score.HitEvents = CreatePositionDistributedHitEvents();
loadPanel(score);
}
[Test]
public void TestScoreWithPositionStatistics()
public void TestScoreWithTimeStatistics()
{
var score = TestResources.CreateTestScoreInfo();
score.HitEvents = createPositionDistributedHitEvents();
score.HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents();
loadPanel(score);
}
@ -79,28 +82,67 @@ namespace osu.Game.Tests.Visual.Ranking
private void loadPanel(ScoreInfo score) => AddStep("load panel", () =>
{
Child = new StatisticsPanel
Child = new SoloStatisticsPanel(score)
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Score = { Value = score }
Score = { Value = score },
StatisticsUpdate =
{
Value = new SoloStatisticsUpdate(score, new UserStatistics
{
Level = new UserStatistics.LevelInfo
{
Current = 5,
Progress = 20,
},
GlobalRank = 38000,
CountryRank = 12006,
PP = 2134,
RankedScore = 21123849,
Accuracy = 0.985,
PlayCount = 13375,
PlayTime = 354490,
TotalScore = 128749597,
TotalHits = 0,
MaxCombo = 1233,
}, new UserStatistics
{
Level = new UserStatistics.LevelInfo
{
Current = 5,
Progress = 30,
},
GlobalRank = 36000,
CountryRank = 12000,
PP = (decimal)2134.5,
RankedScore = 23897015,
Accuracy = 0.984,
PlayCount = 13376,
PlayTime = 35789,
TotalScore = 132218497,
TotalHits = 0,
MaxCombo = 1233,
})
}
};
});
private static List<HitEvent> createPositionDistributedHitEvents()
public static List<HitEvent> CreatePositionDistributedHitEvents()
{
var hitEvents = new List<HitEvent>();
var hitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents();
// Use constant seed for reproducibility
var random = new Random(0);
for (int i = 0; i < 500; i++)
for (int i = 0; i < hitEvents.Count; i++)
{
double angle = random.NextDouble() * 2 * Math.PI;
double radius = random.NextDouble() * 0.5f * OsuHitObject.OBJECT_RADIUS;
var position = new Vector2((float)(radius * Math.Cos(angle)), (float)(radius * Math.Sin(angle)));
hitEvents.Add(new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), position));
hitEvents[i] = hitEvents[i].With(position);
}
return hitEvents;

View File

@ -9,8 +9,8 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
@ -304,11 +304,8 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("preset is not changed", () => panel.Preset.Value.Name == presetName);
AddUntilStep("popover is unchanged", () => this.ChildrenOfType<OsuPopover>().FirstOrDefault() == popover);
AddStep("edit preset name", () => popover.ChildrenOfType<LabelledTextBox>().First().Current.Value = "something new");
AddStep("attempt preset edit", () =>
{
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
AddStep("commit changes to textbox", () => InputManager.Key(Key.Enter));
AddStep("attempt preset edit via select binding", () => InputManager.Key(Key.Enter));
AddUntilStep("popover closed", () => !this.ChildrenOfType<OsuPopover>().Any());
AddAssert("preset is changed", () => panel.Preset.Value.Name != presetName);
}

View File

@ -542,7 +542,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("clear search", () => modSelectOverlay.SearchTerm = string.Empty);
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
AddAssert("mod select still visible", () => modSelectOverlay.State.Value == Visibility.Visible);
}
[Test]

View File

@ -431,8 +431,9 @@ namespace osu.Game.Beatmaps
beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
beatmapInfo.ResetOnlineInfo();
using (var stream = new MemoryStream())
Realm.Write(r =>
{
using var stream = new MemoryStream();
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw);
@ -458,23 +459,20 @@ namespace osu.Game.Beatmaps
updateHashAndMarkDirty(setInfo);
Realm.Write(r =>
{
var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID)!;
var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID)!;
setInfo.CopyChangesToRealm(liveBeatmapSet);
setInfo.CopyChangesToRealm(liveBeatmapSet);
if (transferCollections)
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
if (transferCollections)
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
liveBeatmapSet.Beatmaps.Single(b => b.ID == beatmapInfo.ID)
.UpdateLocalScores(r);
liveBeatmapSet.Beatmaps.Single(b => b.ID == beatmapInfo.ID)
.UpdateLocalScores(r);
// do not look up metadata.
// this is a locally-modified set now, so looking up metadata is busy work at best and harmful at worst.
ProcessBeatmap?.Invoke(liveBeatmapSet, MetadataLookupScope.None);
});
}
// do not look up metadata.
// this is a locally-modified set now, so looking up metadata is busy work at best and harmful at worst.
ProcessBeatmap?.Invoke(liveBeatmapSet, MetadataLookupScope.None);
});
Debug.Assert(beatmapInfo.BeatmapSet != null);

View File

@ -38,6 +38,7 @@ namespace osu.Game.Beatmaps.Legacy
Key1 = 1 << 26,
Key3 = 1 << 27,
Key2 = 1 << 28,
ScoreV2 = 1 << 29,
Mirror = 1 << 30,
}
}

View File

@ -15,17 +15,19 @@ using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Input.Bindings;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Statistics;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Input.Bindings;
using osu.Game.IO.Legacy;
using osu.Game.Models;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@ -79,8 +81,9 @@ namespace osu.Game.Database
/// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes.
/// 30 2023-06-16 Run migration of old lazer scores again. This time with more correct rounding considerations.
/// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores.
/// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files.
/// </summary>
private const int schema_version = 31;
private const int schema_version = 32;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@ -730,6 +733,8 @@ namespace osu.Game.Database
Logger.Log($"Running realm migration to version {targetVersion}...");
Stopwatch stopwatch = new Stopwatch();
var files = new RealmFileStore(this, storage);
stopwatch.Start();
switch (targetVersion)
@ -904,36 +909,17 @@ namespace osu.Game.Database
case 28:
{
var files = new RealmFileStore(this, storage);
var scores = migration.NewRealm.All<ScoreInfo>();
foreach (var score in scores)
{
string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath();
if (replayFilename == null)
continue;
try
score.PopulateFromReplay(files, sr =>
{
using (var stream = files.Store.GetStream(replayFilename))
{
if (stream == null)
continue;
// Trimmed down logic from LegacyScoreDecoder to extract the version from replays.
using (SerializationReader sr = new SerializationReader(stream))
{
sr.ReadByte(); // Ruleset.
int version = sr.ReadInt32();
if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION)
score.IsLegacyScore = true;
}
}
}
catch (Exception e)
{
Logger.Error(e, $"Failed to read replay {replayFilename} during score migration", LoggingTarget.Database);
}
sr.ReadByte(); // Ruleset.
int version = sr.ReadInt32();
if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION)
score.IsLegacyScore = true;
});
}
break;
@ -986,6 +972,46 @@ namespace osu.Game.Database
break;
}
case 32:
{
foreach (var score in migration.NewRealm.All<ScoreInfo>())
{
if (!score.IsLegacyScore || !score.Ruleset.IsLegacyRuleset())
continue;
score.PopulateFromReplay(files, sr =>
{
sr.ReadByte(); // Ruleset.
sr.ReadInt32(); // Version.
sr.ReadString(); // Beatmap hash.
sr.ReadString(); // Username.
sr.ReadString(); // MD5Hash.
sr.ReadUInt16(); // Count300.
sr.ReadUInt16(); // Count100.
sr.ReadUInt16(); // Count50.
sr.ReadUInt16(); // CountGeki.
sr.ReadUInt16(); // CountKatu.
sr.ReadUInt16(); // CountMiss.
// we should have this in LegacyTotalScore already, but if we're reading through this anyways...
int totalScore = sr.ReadInt32();
sr.ReadUInt16(); // Max combo.
sr.ReadBoolean(); // Perfect.
var legacyMods = (LegacyMods)sr.ReadInt32();
if (!legacyMods.HasFlagFast(LegacyMods.ScoreV2) || score.APIMods.Any(mod => mod.Acronym == @"SV2"))
return;
score.APIMods = score.APIMods.Append(new APIMod(new ModScoreV2())).ToArray();
score.LegacyTotalScore = score.TotalScore = totalScore;
});
}
break;
}
}
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");

View File

@ -5,10 +5,14 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.IO.Legacy;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@ -205,6 +209,10 @@ namespace osu.Game.Database
if (ruleset is not ILegacyRuleset legacyRuleset)
return score.TotalScore;
var mods = score.Mods;
if (mods.Any(mod => mod is ModScoreV2))
return score.TotalScore;
var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods);
if (playableBeatmap.HitObjects.Count == 0)
@ -212,7 +220,7 @@ namespace osu.Game.Database
ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator();
sv1Simulator.Simulate(beatmap, playableBeatmap, score.Mods);
sv1Simulator.Simulate(beatmap, playableBeatmap, mods);
return ConvertFromLegacyTotalScore(score, new DifficultyAttributes
{
@ -282,6 +290,38 @@ namespace osu.Game.Database
}
}
/// <summary>
/// Used to populate the <paramref name="score"/> model using data parsed from its corresponding replay file.
/// </summary>
/// <param name="score">The score to run population from replay for.</param>
/// <param name="files">A <see cref="RealmFileStore"/> instance to use for fetching replay.</param>
/// <param name="populationFunc">
/// Delegate describing the population to execute.
/// The delegate's argument is a <see cref="SerializationReader"/> instance which permits to read data from the replay stream.
/// </param>
public static void PopulateFromReplay(this ScoreInfo score, RealmFileStore files, Action<SerializationReader> populationFunc)
{
string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath();
if (replayFilename == null)
return;
try
{
using (var stream = files.Store.GetStream(replayFilename))
{
if (stream == null)
return;
using (SerializationReader sr = new SerializationReader(stream))
populationFunc.Invoke(sr);
}
}
catch (Exception e)
{
Logger.Error(e, $"Failed to read replay {replayFilename} during score migration", LoggingTarget.Database);
}
}
private class FakeHit : HitObject
{
private readonly Judgement judgement;

View File

@ -65,7 +65,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
return base.OnKeyDown(e);
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat)
return false;

View File

@ -8,10 +8,12 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@ -95,6 +97,18 @@ namespace osu.Game.Overlays.Mods
}, true);
}
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
switch (e.Action)
{
case GlobalAction.Select:
createButton.TriggerClick();
return true;
}
return base.OnPressed(e);
}
private void createPreset()
{
realm.Write(r => r.Add(new ModPreset

View File

@ -8,11 +8,13 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Rulesets.Mods;
using osuTK;
@ -130,6 +132,25 @@ namespace osu.Game.Overlays.Mods
}, true);
}
protected override void LoadComplete()
{
base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox));
}
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
switch (e.Action)
{
case GlobalAction.Select:
saveButton.TriggerClick();
return true;
}
return base.OnPressed(e);
}
private void useCurrentMods()
{
saveableMods = selectedMods.Value.ToHashSet();
@ -150,13 +171,6 @@ namespace osu.Game.Overlays.Mods
return !saveableMods.SetEquals(selectedMods.Value);
}
protected override void LoadComplete()
{
base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox));
}
private void save()
{
preset.PerformWrite(s =>

View File

@ -48,7 +48,8 @@ namespace osu.Game.Overlays.Mods
/// Contrary to <see cref="OsuGameBase.AvailableMods"/> and <see cref="globalAvailableMods"/>, the <see cref="Mod"/> instances
/// inside the <see cref="ModState"/> objects are owned solely by this <see cref="ModSelectOverlay"/> instance.
/// </remarks>
public Bindable<Dictionary<ModType, IReadOnlyList<ModState>>> AvailableMods { get; } = new Bindable<Dictionary<ModType, IReadOnlyList<ModState>>>(new Dictionary<ModType, IReadOnlyList<ModState>>());
public Bindable<Dictionary<ModType, IReadOnlyList<ModState>>> AvailableMods { get; } =
new Bindable<Dictionary<ModType, IReadOnlyList<ModState>>>(new Dictionary<ModType, IReadOnlyList<ModState>>());
private Func<Mod, bool> isValidMod = _ => true;
@ -636,12 +637,9 @@ namespace osu.Game.Overlays.Mods
case GlobalAction.Select:
{
// Pressing select should select first filtered mod or completely hide the overlay in one shot if search term is empty.
// Pressing select should select first filtered mod if a search is in progress.
if (string.IsNullOrEmpty(SearchTerm))
{
hideOverlay(true);
return true;
}
ModState? firstMod = columnFlow.Columns.OfType<ModColumn>().FirstOrDefault(m => m.IsPresent)?.AvailableMods.FirstOrDefault(x => x.Visible);

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Rulesets.Difficulty
{
/// <summary>
@ -19,5 +17,16 @@ namespace osu.Game.Rulesets.Difficulty
/// Performance of a perfect play for comparison.
/// </summary>
public PerformanceAttributes PerfectPerformance { get; set; }
/// <summary>
/// Create a new performance breakdown.
/// </summary>
/// <param name="performance">Actual gameplay performance.</param>
/// <param name="perfectPerformance">Performance of a perfect play for comparison.</param>
public PerformanceBreakdown(PerformanceAttributes performance, PerformanceAttributes perfectPerformance)
{
Performance = performance;
PerfectPerformance = perfectPerformance;
}
}
}

View File

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Difficulty
getPerfectPerformance(score, cancellationToken)
).ConfigureAwait(false);
return new PerformanceBreakdown { Performance = performanceArray[0], PerfectPerformance = performanceArray[1] };
return new PerformanceBreakdown(performanceArray[0] ?? new PerformanceAttributes(), performanceArray[1] ?? new PerformanceAttributes());
}
[ItemCanBeNull]

View File

@ -34,6 +34,7 @@ namespace osu.Game.Rulesets.Edit
new CheckUnsnappedObjects(),
new CheckConcurrentObjects(),
new CheckZeroLengthObjects(),
new CheckDrainTime(),
// Timing
new CheckPreviewTime(),

View File

@ -0,0 +1,38 @@
// 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 osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckDrainTime : ICheck
{
private const int min_drain_threshold = 30 * 1000;
public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Compose, "Too short drain time");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateTooShort(this)
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
double drainTime = context.Beatmap.CalculatePlayableLength() - context.Beatmap.TotalBreakTime;
if (drainTime < min_drain_threshold)
yield return new IssueTemplateTooShort(this).Create((int)(drainTime / 1000));
}
public class IssueTemplateTooShort : IssueTemplate
{
public IssueTemplateTooShort(ICheck check)
: base(check, IssueType.Problem, "Less than 30 seconds of drain time, currently {0}.")
{
}
public Issue Create(int drainTimeSeconds) => new Issue(this, drainTimeSeconds);
}
}
}

View File

@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Edit
/// </summary>
protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent?.ReceivePositionalInputAt(screenSpacePos) == true;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
protected override bool Handle(UIEvent e)
{

View File

@ -0,0 +1,20 @@
// 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.Localisation;
namespace osu.Game.Rulesets.Mods
{
/// <remarks>
/// This mod is used strictly to mark osu!stable scores set with the "Score V2" mod active.
/// It should not be used in any real capacity going forward.
/// </remarks>
public class ModScoreV2 : Mod
{
public override string Name => "Score V2";
public override string Acronym => @"SV2";
public override ModType Type => ModType.System;
public override LocalisableString Description => "Score set on earlier osu! versions with the V2 scoring algorithm active.";
public override double ScoreMultiplier => 1;
}
}

View File

@ -192,6 +192,10 @@ namespace osu.Game.Rulesets
case ModAutoplay:
value |= LegacyMods.Autoplay;
break;
case ModScoreV2:
value |= LegacyMods.ScoreV2;
break;
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Extensions.TypeExtensions;
@ -137,30 +138,17 @@ namespace osu.Game.Rulesets.Scoring
JudgedHits += count;
}
/// <summary>
/// Creates the <see cref="JudgementResult"/> that represents the scoring result for a <see cref="HitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> which was judged.</param>
/// <param name="judgement">The <see cref="Judgement"/> that provides the scoring information.</param>
protected virtual JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new JudgementResult(hitObject, judgement);
/// <summary>
/// Simulates an autoplay of the <see cref="IBeatmap"/> to determine scoring values.
/// </summary>
/// <remarks>This provided temporarily. DO NOT USE.</remarks>
/// <param name="beatmap">The <see cref="IBeatmap"/> to simulate.</param>
protected virtual void SimulateAutoplay(IBeatmap beatmap)
protected void SimulateAutoplay(IBeatmap beatmap)
{
IsSimulating = true;
foreach (var obj in beatmap.HitObjects)
simulate(obj);
void simulate(HitObject obj)
foreach (var obj in EnumerateHitObjects(beatmap))
{
foreach (var nested in obj.NestedHitObjects)
simulate(nested);
var judgement = obj.CreateJudgement();
var result = CreateResult(obj, judgement);
@ -174,6 +162,43 @@ namespace osu.Game.Rulesets.Scoring
IsSimulating = false;
}
/// <summary>
/// Enumerates all <see cref="HitObject"/>s in the given <paramref name="beatmap"/> in the order in which they are to be judged.
/// Used in <see cref="SimulateAutoplay"/>.
/// </summary>
/// <remarks>
/// In Score V2, the score awarded for each object includes a component based on the combo value after the judgement of that object.
/// This means that the score is dependent on the order of evaluation of judgements.
/// This method is provided so that rulesets can specify custom ordering that is correct for them and matches processing order during actual gameplay.
/// </remarks>
protected virtual IEnumerable<HitObject> EnumerateHitObjects(IBeatmap beatmap)
=> enumerateRecursively(beatmap.HitObjects);
private IEnumerable<HitObject> enumerateRecursively(IEnumerable<HitObject> hitObjects)
{
foreach (var hitObject in hitObjects)
{
foreach (var nested in enumerateRecursively(hitObject.NestedHitObjects))
yield return nested;
yield return hitObject;
}
}
/// <summary>
/// Creates the <see cref="JudgementResult"/> that represents the scoring result for a <see cref="HitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> which was judged.</param>
/// <param name="judgement">The <see cref="Judgement"/> that provides the scoring information.</param>
protected virtual JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new JudgementResult(hitObject, judgement);
/// <summary>
/// Gets a simulated <see cref="HitResult"/> for a judgement. Used during <see cref="SimulateAutoplay"/> to simulate a "perfect" play.
/// </summary>
/// <param name="judgement">The judgement to simulate a <see cref="HitResult"/> for.</param>
/// <returns>The simulated <see cref="HitResult"/> for the judgement.</returns>
protected virtual HitResult GetSimulatedHitResult(Judgement judgement) => judgement.MaxResult;
protected override void Update()
{
base.Update();
@ -184,12 +209,5 @@ namespace osu.Game.Rulesets.Scoring
// Last applied result is guaranteed to be non-null when JudgedHits > 0.
|| lastAppliedResult.AsNonNull().TimeAbsolute < Clock.CurrentTime);
}
/// <summary>
/// Gets a simulated <see cref="HitResult"/> for a judgement. Used during <see cref="SimulateAutoplay"/> to simulate a "perfect" play.
/// </summary>
/// <param name="judgement">The judgement to simulate a <see cref="HitResult"/> for.</param>
/// <returns>The simulated <see cref="HitResult"/> for the judgement.</returns>
protected virtual HitResult GetSimulatedHitResult(Judgement judgement) => judgement.MaxResult;
}
}

View File

@ -5,9 +5,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Layout;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Scoring;
@ -113,94 +115,95 @@ namespace osu.Game.Screens.Ranking.Statistics
}
}
if (barDrawables != null)
{
for (int i = 0; i < barDrawables.Length; i++)
{
barDrawables[i].UpdateOffset(bins[i].Sum(b => b.Value));
}
}
if (barDrawables == null)
createBarDrawables();
else
{
int maxCount = bins.Max(b => b.Values.Sum());
barDrawables = bins.Select((bin, i) => new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index)).ToArray();
for (int i = 0; i < barDrawables.Length; i++)
barDrawables[i].UpdateOffset(bins[i].Sum(b => b.Value));
}
}
Container axisFlow;
private void createBarDrawables()
{
int maxCount = bins.Max(b => b.Values.Sum());
barDrawables = bins.Select((_, i) => new Bar(bins[i], maxCount, i == timing_distribution_centre_bin_index)).ToArray();
const float axis_font_size = 12;
Container axisFlow;
InternalChild = new GridContainer
const float axis_font_size = 12;
InternalChild = new GridContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Width = 0.8f,
Content = new[]
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Width = 0.8f,
Content = new[]
new Drawable[]
{
new Drawable[]
new GridContainer
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[] { barDrawables }
}
},
new Drawable[]
{
axisFlow = new Container
{
RelativeSizeAxes = Axes.X,
Height = axis_font_size,
}
},
RelativeSizeAxes = Axes.Both,
Content = new[] { barDrawables }
}
},
RowDimensions = new[]
new Drawable[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
}
};
axisFlow = new Container
{
RelativeSizeAxes = Axes.X,
Height = axis_font_size,
}
},
},
RowDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
}
};
// Our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size.
double maxValue = timing_distribution_bins * binSize;
double axisValueStep = maxValue / axis_points;
// Our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size.
double maxValue = timing_distribution_bins * binSize;
double axisValueStep = maxValue / axis_points;
axisFlow.Add(new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "0",
Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold)
});
for (int i = 1; i <= axis_points; i++)
{
double axisValue = i * axisValueStep;
float position = (float)(axisValue / maxValue);
float alpha = 1f - position * 0.8f;
axisFlow.Add(new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "0",
RelativePositionAxes = Axes.X,
X = -position / 2,
Alpha = alpha,
Text = axisValue.ToString("-0"),
Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold)
});
for (int i = 1; i <= axis_points; i++)
axisFlow.Add(new OsuSpriteText
{
double axisValue = i * axisValueStep;
float position = (float)(axisValue / maxValue);
float alpha = 1f - position * 0.8f;
axisFlow.Add(new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.X,
X = -position / 2,
Alpha = alpha,
Text = axisValue.ToString("-0"),
Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold)
});
axisFlow.Add(new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.X,
X = position / 2,
Alpha = alpha,
Text = axisValue.ToString("+0"),
Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold)
});
}
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.X,
X = position / 2,
Alpha = alpha,
Text = axisValue.ToString("+0"),
Font = OsuFont.GetFont(size: axis_font_size, weight: FontWeight.SemiBold)
});
}
}
@ -211,13 +214,16 @@ namespace osu.Game.Screens.Ranking.Statistics
private readonly bool isCentre;
private readonly float totalValue;
private float basalHeight;
private const float minimum_height = 0.02f;
private float offsetAdjustment;
private Circle[] boxOriginals = null!;
private Circle? boxAdjustment;
private float? lastDrawHeight;
[Resolved]
private OsuColour colours { get; set; } = null!;
@ -256,15 +262,17 @@ namespace osu.Game.Screens.Ranking.Statistics
else
{
// A bin with no value draws a grey dot instead.
Circle dot = new Circle
InternalChildren = boxOriginals = new[]
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Colour = isCentre ? Color4.White : Color4.Gray,
Height = 0,
new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Colour = isCentre ? Color4.White : Color4.Gray,
Height = 0,
}
};
InternalChildren = boxOriginals = new[] { dot };
}
}
@ -272,31 +280,18 @@ namespace osu.Game.Screens.Ranking.Statistics
{
base.LoadComplete();
if (!values.Any())
return;
updateBasalHeight();
foreach (var boxOriginal in boxOriginals)
{
boxOriginal.Y = 0;
boxOriginal.Height = basalHeight;
}
float offsetValue = 0;
for (int i = 0; i < values.Count; i++)
{
boxOriginals[i].MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint);
boxOriginals[i].ResizeHeightTo(heightForValue(values[i].Value), duration, Easing.OutQuint);
offsetValue -= values[i].Value;
}
Scheduler.AddOnce(updateMetrics, true);
}
protected override void Update()
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
{
base.Update();
updateBasalHeight();
if (invalidation.HasFlagFast(Invalidation.DrawSize))
{
if (lastDrawHeight != null && lastDrawHeight != DrawHeight)
Scheduler.AddOnce(updateMetrics, false);
}
return base.OnInvalidate(invalidation, source);
}
public void UpdateOffset(float adjustment)
@ -321,45 +316,32 @@ namespace osu.Game.Screens.Ranking.Statistics
}
offsetAdjustment = adjustment;
drawAdjustmentBar();
Scheduler.AddOnce(updateMetrics, true);
}
private void updateBasalHeight()
{
float newBasalHeight = DrawHeight > DrawWidth ? DrawWidth / DrawHeight : 1;
if (newBasalHeight == basalHeight)
return;
basalHeight = newBasalHeight;
foreach (var dot in boxOriginals)
dot.Height = basalHeight;
draw();
}
private float offsetForValue(float value) => (1 - basalHeight) * value / maxValue;
private float heightForValue(float value) => MathF.Max(basalHeight + offsetForValue(value), 0);
private void draw()
{
resizeBars();
if (boxAdjustment != null)
drawAdjustmentBar();
}
private void resizeBars()
private void updateMetrics(bool animate = true)
{
float offsetValue = 0;
for (int i = 0; i < values.Count; i++)
for (int i = 0; i < boxOriginals.Length; i++)
{
boxOriginals[i].Y = offsetForValue(offsetValue) * DrawHeight;
boxOriginals[i].Height = heightForValue(values[i].Value);
offsetValue -= values[i].Value;
int value = i < values.Count ? values[i].Value : 0;
var box = boxOriginals[i];
box.MoveToY(offsetForValue(offsetValue) * BoundingBox.Height, duration, Easing.OutQuint);
box.ResizeHeightTo(heightForValue(value), duration, Easing.OutQuint);
offsetValue -= value;
}
if (boxAdjustment != null)
drawAdjustmentBar();
if (!animate)
FinishTransforms(true);
lastDrawHeight = DrawHeight;
}
private void drawAdjustmentBar()
@ -369,6 +351,10 @@ namespace osu.Game.Screens.Ranking.Statistics
boxAdjustment.ResizeHeightTo(heightForValue(offsetAdjustment), duration, Easing.OutQuint);
boxAdjustment.FadeTo(!hasAdjustment ? 0 : 1, duration, Easing.OutQuint);
}
private float offsetForValue(float value) => (1 - minimum_height) * value / maxValue;
private float heightForValue(float value) => minimum_height + offsetForValue(value);
}
}
}

View File

@ -31,7 +31,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Toolkit.HighPerformance" Version="7.1.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="ppy.LocalisationAnalyser" Version="2022.809.0">
<PackageReference Include="ppy.LocalisationAnalyser" Version="2023.712.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>