mirror of
https://github.com/ppy/osu.git
synced 2024-09-21 17:27:24 +08:00
Merge branch 'master' into framework-IWindow-changes
This commit is contained in:
commit
1d9d90e6e3
@ -187,7 +187,7 @@ namespace osu.Desktop
|
|||||||
return edit.BeatmapInfo.ToString() ?? string.Empty;
|
return edit.BeatmapInfo.ToString() ?? string.Empty;
|
||||||
|
|
||||||
case UserActivity.WatchingReplay watching:
|
case UserActivity.WatchingReplay watching:
|
||||||
return watching.BeatmapInfo.ToString();
|
return watching.BeatmapInfo?.ToString() ?? string.Empty;
|
||||||
|
|
||||||
case UserActivity.InLobby lobby:
|
case UserActivity.InLobby lobby:
|
||||||
return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value;
|
return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value;
|
||||||
|
@ -202,6 +202,8 @@ namespace osu.Game.Rulesets.Catch
|
|||||||
|
|
||||||
public int LegacyID => 2;
|
public int LegacyID => 2;
|
||||||
|
|
||||||
|
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new CatchLegacyScoreSimulator();
|
||||||
|
|
||||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
|
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
|
||||||
|
|
||||||
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
|
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
|
||||||
|
@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
// Todo: osu!catch should not output star rating in the 'aim' attribute.
|
// Todo: osu!catch should not output star rating in the 'aim' attribute.
|
||||||
yield return (ATTRIB_ID_AIM, StarRating);
|
yield return (ATTRIB_ID_AIM, StarRating);
|
||||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||||
@ -36,7 +35,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
|
|
||||||
StarRating = values[ATTRIB_ID_AIM];
|
StarRating = values[ATTRIB_ID_AIM];
|
||||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,9 +25,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
|
|
||||||
public override int Version => 20220701;
|
public override int Version => 20220701;
|
||||||
|
|
||||||
|
private readonly IWorkingBeatmap workingBeatmap;
|
||||||
|
|
||||||
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
|
workingBeatmap = beatmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
@ -38,13 +41,24 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||||
|
|
||||||
return new CatchDifficultyAttributes
|
CatchDifficultyAttributes attributes = new CatchDifficultyAttributes
|
||||||
{
|
{
|
||||||
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor,
|
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor,
|
||||||
Mods = mods,
|
Mods = mods,
|
||||||
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
||||||
MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)),
|
MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (ComputeLegacyScoringValues)
|
||||||
|
{
|
||||||
|
CatchLegacyScoreSimulator sv1Simulator = new CatchLegacyScoreSimulator();
|
||||||
|
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
||||||
|
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
||||||
|
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
||||||
|
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
|
142
osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs
Normal file
142
osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||||
|
{
|
||||||
|
internal class CatchLegacyScoreSimulator : ILegacyScoreSimulator
|
||||||
|
{
|
||||||
|
public int AccuracyScore { get; private set; }
|
||||||
|
|
||||||
|
public int ComboScore { get; private set; }
|
||||||
|
|
||||||
|
public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore;
|
||||||
|
|
||||||
|
private int legacyBonusScore;
|
||||||
|
private int modernBonusScore;
|
||||||
|
private int combo;
|
||||||
|
|
||||||
|
private double scoreMultiplier;
|
||||||
|
|
||||||
|
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
||||||
|
{
|
||||||
|
IBeatmap baseBeatmap = workingBeatmap.Beatmap;
|
||||||
|
|
||||||
|
int countNormal = 0;
|
||||||
|
int countSlider = 0;
|
||||||
|
int countSpinner = 0;
|
||||||
|
|
||||||
|
foreach (HitObject obj in baseBeatmap.HitObjects)
|
||||||
|
{
|
||||||
|
switch (obj)
|
||||||
|
{
|
||||||
|
case IHasPath:
|
||||||
|
countSlider++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case IHasDuration:
|
||||||
|
countSpinner++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
countNormal++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int objectCount = countNormal + countSlider + countSpinner;
|
||||||
|
|
||||||
|
int drainLength = 0;
|
||||||
|
|
||||||
|
if (baseBeatmap.HitObjects.Count > 0)
|
||||||
|
{
|
||||||
|
int breakLength = baseBeatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum();
|
||||||
|
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
int difficultyPeppyStars = (int)Math.Round(
|
||||||
|
(baseBeatmap.Difficulty.DrainRate
|
||||||
|
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||||
|
+ baseBeatmap.Difficulty.CircleSize
|
||||||
|
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||||
|
|
||||||
|
scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
||||||
|
|
||||||
|
foreach (var obj in playableBeatmap.HitObjects)
|
||||||
|
simulateHit(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void simulateHit(HitObject hitObject)
|
||||||
|
{
|
||||||
|
bool increaseCombo = true;
|
||||||
|
bool addScoreComboMultiplier = false;
|
||||||
|
|
||||||
|
bool isBonus = false;
|
||||||
|
HitResult bonusResult = HitResult.None;
|
||||||
|
|
||||||
|
int scoreIncrease = 0;
|
||||||
|
|
||||||
|
switch (hitObject)
|
||||||
|
{
|
||||||
|
case TinyDroplet:
|
||||||
|
scoreIncrease = 10;
|
||||||
|
increaseCombo = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Droplet:
|
||||||
|
scoreIncrease = 100;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Fruit:
|
||||||
|
scoreIncrease = 300;
|
||||||
|
addScoreComboMultiplier = true;
|
||||||
|
increaseCombo = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Banana:
|
||||||
|
scoreIncrease = 1100;
|
||||||
|
increaseCombo = false;
|
||||||
|
isBonus = true;
|
||||||
|
bonusResult = HitResult.LargeBonus;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case JuiceStream:
|
||||||
|
foreach (var nested in hitObject.NestedHitObjects)
|
||||||
|
simulateHit(nested);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case BananaShower:
|
||||||
|
foreach (var nested in hitObject.NestedHitObjects)
|
||||||
|
simulateHit(nested);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addScoreComboMultiplier)
|
||||||
|
{
|
||||||
|
// ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...)
|
||||||
|
ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBonus)
|
||||||
|
{
|
||||||
|
legacyBonusScore += scoreIncrease;
|
||||||
|
modernBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
AccuracyScore += scoreIncrease;
|
||||||
|
|
||||||
|
if (increaseCombo)
|
||||||
|
combo++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -24,7 +24,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
foreach (var v in base.ToDatabaseAttributes())
|
foreach (var v in base.ToDatabaseAttributes())
|
||||||
yield return v;
|
yield return v;
|
||||||
|
|
||||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
|
||||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||||
}
|
}
|
||||||
@ -33,7 +32,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
{
|
{
|
||||||
base.FromDatabaseAttributes(values, onlineInfo);
|
base.FromDatabaseAttributes(values, onlineInfo);
|
||||||
|
|
||||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
|
||||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||||
}
|
}
|
||||||
|
@ -31,9 +31,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
|
|
||||||
public override int Version => 20220902;
|
public override int Version => 20220902;
|
||||||
|
|
||||||
|
private readonly IWorkingBeatmap workingBeatmap;
|
||||||
|
|
||||||
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
|
workingBeatmap = beatmap;
|
||||||
|
|
||||||
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset);
|
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset);
|
||||||
originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty;
|
originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty;
|
||||||
}
|
}
|
||||||
@ -46,15 +50,26 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
HitWindows hitWindows = new ManiaHitWindows();
|
HitWindows hitWindows = new ManiaHitWindows();
|
||||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||||
|
|
||||||
return new ManiaDifficultyAttributes
|
ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes
|
||||||
{
|
{
|
||||||
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
|
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
|
||||||
Mods = mods,
|
Mods = mods,
|
||||||
// In osu-stable mania, rate-adjustment mods don't affect the hit window.
|
// In osu-stable mania, rate-adjustment mods don't affect the hit window.
|
||||||
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
|
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
|
||||||
GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
|
GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
|
||||||
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject)
|
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (ComputeLegacyScoringValues)
|
||||||
|
{
|
||||||
|
ManiaLegacyScoreSimulator sv1Simulator = new ManiaLegacyScoreSimulator();
|
||||||
|
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
||||||
|
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
||||||
|
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
||||||
|
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int maxComboForObject(HitObject hitObject)
|
private static int maxComboForObject(HitObject hitObject)
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Mods;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Difficulty
|
||||||
|
{
|
||||||
|
internal class ManiaLegacyScoreSimulator : ILegacyScoreSimulator
|
||||||
|
{
|
||||||
|
public int AccuracyScore => 0;
|
||||||
|
public int ComboScore { get; private set; }
|
||||||
|
public double BonusScoreRatio => 0;
|
||||||
|
|
||||||
|
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
||||||
|
{
|
||||||
|
double multiplier = mods.Where(m => m is not (ModHidden or ModHardRock or ModDoubleTime or ModFlashlight or ManiaModFadeIn))
|
||||||
|
.Select(m => m.ScoreMultiplier)
|
||||||
|
.Aggregate(1.0, (c, n) => c * n);
|
||||||
|
|
||||||
|
ComboScore = (int)(1000000 * multiplier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -302,6 +302,8 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
|
|
||||||
public int LegacyID => 3;
|
public int LegacyID => 3;
|
||||||
|
|
||||||
|
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new ManiaLegacyScoreSimulator();
|
||||||
|
|
||||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame();
|
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame();
|
||||||
|
|
||||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new ManiaRulesetConfigManager(settings, RulesetInfo);
|
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new ManiaRulesetConfigManager(settings, RulesetInfo);
|
||||||
|
@ -93,7 +93,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
yield return (ATTRIB_ID_SPEED, SpeedDifficulty);
|
yield return (ATTRIB_ID_SPEED, SpeedDifficulty);
|
||||||
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
|
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
|
||||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
|
||||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||||
|
|
||||||
if (ShouldSerializeFlashlightRating())
|
if (ShouldSerializeFlashlightRating())
|
||||||
@ -111,7 +110,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
SpeedDifficulty = values[ATTRIB_ID_SPEED];
|
SpeedDifficulty = values[ATTRIB_ID_SPEED];
|
||||||
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
|
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
|
||||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
|
||||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||||
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
||||||
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
||||||
|
@ -26,9 +26,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
public override int Version => 20220902;
|
public override int Version => 20220902;
|
||||||
|
|
||||||
|
private readonly IWorkingBeatmap workingBeatmap;
|
||||||
|
|
||||||
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
|
workingBeatmap = beatmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
@ -71,7 +74,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1
|
Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1
|
||||||
);
|
);
|
||||||
|
|
||||||
double starRating = basePerformance > 0.00001 ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
|
double starRating = basePerformance > 0.00001
|
||||||
|
? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4)
|
||||||
|
: 0;
|
||||||
|
|
||||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||||
double drainRate = beatmap.Difficulty.DrainRate;
|
double drainRate = beatmap.Difficulty.DrainRate;
|
||||||
@ -86,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||||
|
|
||||||
return new OsuDifficultyAttributes
|
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
|
||||||
{
|
{
|
||||||
StarRating = starRating,
|
StarRating = starRating,
|
||||||
Mods = mods,
|
Mods = mods,
|
||||||
@ -103,6 +108,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
SliderCount = sliderCount,
|
SliderCount = sliderCount,
|
||||||
SpinnerCount = spinnerCount,
|
SpinnerCount = spinnerCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (ComputeLegacyScoringValues)
|
||||||
|
{
|
||||||
|
OsuLegacyScoreSimulator sv1Simulator = new OsuLegacyScoreSimulator();
|
||||||
|
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
||||||
|
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
||||||
|
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
||||||
|
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
|
177
osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs
Normal file
177
osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||||
|
{
|
||||||
|
internal class OsuLegacyScoreSimulator : ILegacyScoreSimulator
|
||||||
|
{
|
||||||
|
public int AccuracyScore { get; private set; }
|
||||||
|
|
||||||
|
public int ComboScore { get; private set; }
|
||||||
|
|
||||||
|
public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore;
|
||||||
|
|
||||||
|
private int legacyBonusScore;
|
||||||
|
private int modernBonusScore;
|
||||||
|
private int combo;
|
||||||
|
|
||||||
|
private double scoreMultiplier;
|
||||||
|
private IBeatmap playableBeatmap = null!;
|
||||||
|
|
||||||
|
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
||||||
|
{
|
||||||
|
this.playableBeatmap = playableBeatmap;
|
||||||
|
|
||||||
|
IBeatmap baseBeatmap = workingBeatmap.Beatmap;
|
||||||
|
|
||||||
|
int countNormal = 0;
|
||||||
|
int countSlider = 0;
|
||||||
|
int countSpinner = 0;
|
||||||
|
|
||||||
|
foreach (HitObject obj in workingBeatmap.Beatmap.HitObjects)
|
||||||
|
{
|
||||||
|
switch (obj)
|
||||||
|
{
|
||||||
|
case IHasPath:
|
||||||
|
countSlider++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case IHasDuration:
|
||||||
|
countSpinner++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
countNormal++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int objectCount = countNormal + countSlider + countSpinner;
|
||||||
|
|
||||||
|
int drainLength = 0;
|
||||||
|
|
||||||
|
if (baseBeatmap.HitObjects.Count > 0)
|
||||||
|
{
|
||||||
|
int breakLength = baseBeatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum();
|
||||||
|
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
int difficultyPeppyStars = (int)Math.Round(
|
||||||
|
(baseBeatmap.Difficulty.DrainRate
|
||||||
|
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||||
|
+ baseBeatmap.Difficulty.CircleSize
|
||||||
|
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||||
|
|
||||||
|
scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
||||||
|
|
||||||
|
foreach (var obj in playableBeatmap.HitObjects)
|
||||||
|
simulateHit(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void simulateHit(HitObject hitObject)
|
||||||
|
{
|
||||||
|
bool increaseCombo = true;
|
||||||
|
bool addScoreComboMultiplier = false;
|
||||||
|
|
||||||
|
bool isBonus = false;
|
||||||
|
HitResult bonusResult = HitResult.None;
|
||||||
|
|
||||||
|
int scoreIncrease = 0;
|
||||||
|
|
||||||
|
switch (hitObject)
|
||||||
|
{
|
||||||
|
case SliderHeadCircle:
|
||||||
|
case SliderTailCircle:
|
||||||
|
case SliderRepeat:
|
||||||
|
scoreIncrease = 30;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SliderTick:
|
||||||
|
scoreIncrease = 10;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SpinnerBonusTick:
|
||||||
|
scoreIncrease = 1100;
|
||||||
|
increaseCombo = false;
|
||||||
|
isBonus = true;
|
||||||
|
bonusResult = HitResult.LargeBonus;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SpinnerTick:
|
||||||
|
scoreIncrease = 100;
|
||||||
|
increaseCombo = false;
|
||||||
|
isBonus = true;
|
||||||
|
bonusResult = HitResult.SmallBonus;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HitCircle:
|
||||||
|
scoreIncrease = 300;
|
||||||
|
addScoreComboMultiplier = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Slider:
|
||||||
|
foreach (var nested in hitObject.NestedHitObjects)
|
||||||
|
simulateHit(nested);
|
||||||
|
|
||||||
|
scoreIncrease = 300;
|
||||||
|
increaseCombo = false;
|
||||||
|
addScoreComboMultiplier = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Spinner spinner:
|
||||||
|
// The spinner object applies a lenience because gameplay mechanics differ from osu-stable.
|
||||||
|
// We'll redo the calculations to match osu-stable here...
|
||||||
|
const double maximum_rotations_per_second = 477.0 / 60;
|
||||||
|
double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(playableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5);
|
||||||
|
double secondsDuration = spinner.Duration / 1000;
|
||||||
|
|
||||||
|
// The total amount of half spins possible for the entire spinner.
|
||||||
|
int totalHalfSpinsPossible = (int)(secondsDuration * maximum_rotations_per_second * 2);
|
||||||
|
// The amount of half spins that are required to successfully complete the spinner (i.e. get a 300).
|
||||||
|
int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond);
|
||||||
|
// To be able to receive bonus points, the spinner must be rotated another 1.5 times.
|
||||||
|
int halfSpinsRequiredBeforeBonus = halfSpinsRequiredForCompletion + 3;
|
||||||
|
|
||||||
|
for (int i = 0; i <= totalHalfSpinsPossible; i++)
|
||||||
|
{
|
||||||
|
if (i > halfSpinsRequiredBeforeBonus && (i - halfSpinsRequiredBeforeBonus) % 2 == 0)
|
||||||
|
simulateHit(new SpinnerBonusTick());
|
||||||
|
else if (i > 1 && i % 2 == 0)
|
||||||
|
simulateHit(new SpinnerTick());
|
||||||
|
}
|
||||||
|
|
||||||
|
scoreIncrease = 300;
|
||||||
|
addScoreComboMultiplier = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addScoreComboMultiplier)
|
||||||
|
{
|
||||||
|
// ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...)
|
||||||
|
ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBonus)
|
||||||
|
{
|
||||||
|
legacyBonusScore += scoreIncrease;
|
||||||
|
modernBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
AccuracyScore += scoreIncrease;
|
||||||
|
|
||||||
|
if (increaseCombo)
|
||||||
|
combo++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -253,6 +253,8 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
|
|
||||||
public int LegacyID => 0;
|
public int LegacyID => 0;
|
||||||
|
|
||||||
|
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new OsuLegacyScoreSimulator();
|
||||||
|
|
||||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame();
|
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame();
|
||||||
|
|
||||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
|
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
|
||||||
|
@ -72,13 +72,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
|
|
||||||
seekTo(200);
|
seekTo(200);
|
||||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -100,13 +100,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
|
|
||||||
seekTo(200);
|
seekTo(200);
|
||||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -145,23 +145,23 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first));
|
AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first));
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
|
||||||
|
|
||||||
seekTo(120);
|
seekTo(120);
|
||||||
AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first));
|
AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first));
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
|
||||||
|
|
||||||
seekTo(480);
|
seekTo(480);
|
||||||
AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second));
|
AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second));
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
|
|
||||||
seekTo(700);
|
seekTo(700);
|
||||||
AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second));
|
AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second));
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -174,8 +174,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
StartTime = 100,
|
StartTime = 100,
|
||||||
Samples = new List<HitSampleInfo>
|
Samples = new List<HitSampleInfo>
|
||||||
{
|
{
|
||||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"),
|
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM),
|
||||||
new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong
|
new HitSampleInfo(HitSampleInfo.HIT_FINISH, HitSampleInfo.BANK_DRUM) // implies strong
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
@ -184,13 +184,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("most valid object is nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit.StrongNestedHit>);
|
AddAssert("most valid object is nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit.StrongNestedHit>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
|
||||||
|
|
||||||
seekTo(200);
|
seekTo(200);
|
||||||
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -213,18 +213,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
|
|
||||||
seekTo(600);
|
seekTo(600);
|
||||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
|
|
||||||
seekTo(1200);
|
seekTo(1200);
|
||||||
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -247,18 +247,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
|
|
||||||
seekTo(600);
|
seekTo(600);
|
||||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
|
|
||||||
seekTo(1200);
|
seekTo(1200);
|
||||||
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -272,8 +272,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
EndTime = 1100,
|
EndTime = 1100,
|
||||||
Samples = new List<HitSampleInfo>
|
Samples = new List<HitSampleInfo>
|
||||||
{
|
{
|
||||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"),
|
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM),
|
||||||
new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong
|
new HitSampleInfo(HitSampleInfo.HIT_FINISH, HitSampleInfo.BANK_DRUM) // implies strong
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
@ -282,18 +282,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
|
||||||
|
|
||||||
seekTo(600);
|
seekTo(600);
|
||||||
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
|
||||||
|
|
||||||
seekTo(1200);
|
seekTo(1200);
|
||||||
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -319,18 +319,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
|
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
|
||||||
// But for sample playback purposes they can be ignored as noise.
|
// But for sample playback purposes they can be ignored as noise.
|
||||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
|
|
||||||
seekTo(600);
|
seekTo(600);
|
||||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
|
|
||||||
seekTo(1200);
|
seekTo(1200);
|
||||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -344,7 +344,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
EndTime = 1100,
|
EndTime = 1100,
|
||||||
Samples = new List<HitSampleInfo>
|
Samples = new List<HitSampleInfo>
|
||||||
{
|
{
|
||||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum")
|
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
@ -356,25 +356,26 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
|
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
|
||||||
// But for sample playback purposes they can be ignored as noise.
|
// But for sample playback purposes they can be ignored as noise.
|
||||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
|
||||||
|
|
||||||
seekTo(600);
|
seekTo(600);
|
||||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
|
||||||
|
|
||||||
seekTo(1200);
|
seekTo(1200);
|
||||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||||
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
|
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
|
||||||
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
|
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkSound(HitType hitType, string expectedName, string expectedBank)
|
private void checkSamples(HitType hitType, string expectedSamplesCsv, string expectedBank)
|
||||||
{
|
{
|
||||||
AddStep($"hit {hitType}", () => triggerSource.Play(hitType));
|
AddStep($"hit {hitType}", () => triggerSource.Play(hitType));
|
||||||
AddAssert($"last played sample is {expectedName}", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Single().Name, () => Is.EqualTo(expectedName));
|
AddAssert($"last played sample is {expectedSamplesCsv}", () => string.Join(',', triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Select(s => s.Name)),
|
||||||
AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Single().Bank, () => Is.EqualTo(expectedBank));
|
() => Is.EqualTo(expectedSamplesCsv));
|
||||||
|
AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().First().Bank, () => Is.EqualTo(expectedBank));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void seekTo(double time) => AddStep($"seek to {time}", () => gameplayClock.Seek(time));
|
private void seekTo(double time) => AddStep($"seek to {time}", () => gameplayClock.Seek(time));
|
||||||
|
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
|
|
||||||
private void addFlyingHit(HitType hitType)
|
private void addFlyingHit(HitType hitType)
|
||||||
{
|
{
|
||||||
var tick = new DrumRollTick { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current };
|
var tick = new DrumRollTick(new DrumRoll()) { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current };
|
||||||
|
|
||||||
DrawableDrumRollTick h;
|
DrawableDrumRollTick h;
|
||||||
DrawableRuleset.Playfield.Add(h = new DrawableDrumRollTick(tick) { JudgementType = hitType });
|
DrawableRuleset.Playfield.Add(h = new DrawableDrumRollTick(tick) { JudgementType = hitType });
|
||||||
|
@ -3,15 +3,16 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Audio;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Taiko.UI;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Tests
|
namespace osu.Game.Rulesets.Taiko.Tests
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Taiko has some interesting rules for legacy mappings.
|
/// Taiko doesn't output any samples. They are all handled externally by <see cref="DrumSamplePlayer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HeadlessTest]
|
[HeadlessTest]
|
||||||
public partial class TestSceneSampleOutput : TestSceneTaikoPlayer
|
public partial class TestSceneSampleOutput : TestSceneTaikoPlayer
|
||||||
@ -26,10 +27,10 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
string.Empty,
|
string.Empty,
|
||||||
string.Empty,
|
string.Empty,
|
||||||
string.Empty,
|
string.Empty,
|
||||||
HitSampleInfo.HIT_FINISH,
|
string.Empty,
|
||||||
HitSampleInfo.HIT_WHISTLE,
|
string.Empty,
|
||||||
HitSampleInfo.HIT_WHISTLE,
|
string.Empty,
|
||||||
HitSampleInfo.HIT_WHISTLE,
|
string.Empty,
|
||||||
};
|
};
|
||||||
|
|
||||||
var actualSampleNames = new List<string>();
|
var actualSampleNames = new List<string>();
|
||||||
@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
|
|
||||||
AddUntilStep("all samples collected", () => actualSampleNames.Count == expectedSampleNames.Length);
|
AddUntilStep("all samples collected", () => actualSampleNames.Count == expectedSampleNames.Length);
|
||||||
|
|
||||||
AddAssert("samples are correct", () => actualSampleNames.SequenceEqual(expectedSampleNames));
|
AddAssert("samples are correct", () => actualSampleNames, () => Is.EqualTo(expectedSampleNames));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TaikoBeatmapConversionTest().GetBeatmap("sample-to-type-conversions");
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TaikoBeatmapConversionTest().GetBeatmap("sample-to-type-conversions");
|
||||||
|
@ -48,7 +48,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
foreach (var v in base.ToDatabaseAttributes())
|
foreach (var v in base.ToDatabaseAttributes())
|
||||||
yield return v;
|
yield return v;
|
||||||
|
|
||||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
|
||||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||||
}
|
}
|
||||||
@ -57,7 +56,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
{
|
{
|
||||||
base.FromDatabaseAttributes(values, onlineInfo);
|
base.FromDatabaseAttributes(values, onlineInfo);
|
||||||
|
|
||||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
|
||||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||||
}
|
}
|
||||||
|
@ -25,9 +25,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
|
|
||||||
public override int Version => 20220902;
|
public override int Version => 20220902;
|
||||||
|
|
||||||
|
private readonly IWorkingBeatmap workingBeatmap;
|
||||||
|
|
||||||
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
|
workingBeatmap = beatmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||||
@ -84,7 +87,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
HitWindows hitWindows = new TaikoHitWindows();
|
HitWindows hitWindows = new TaikoHitWindows();
|
||||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||||
|
|
||||||
return new TaikoDifficultyAttributes
|
TaikoDifficultyAttributes attributes = new TaikoDifficultyAttributes
|
||||||
{
|
{
|
||||||
StarRating = starRating,
|
StarRating = starRating,
|
||||||
Mods = mods,
|
Mods = mods,
|
||||||
@ -95,6 +98,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
|
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
|
||||||
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
|
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (ComputeLegacyScoringValues)
|
||||||
|
{
|
||||||
|
TaikoLegacyScoreSimulator sv1Simulator = new TaikoLegacyScoreSimulator();
|
||||||
|
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
||||||
|
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
||||||
|
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
||||||
|
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
202
osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs
Normal file
202
osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||||
|
{
|
||||||
|
internal class TaikoLegacyScoreSimulator : ILegacyScoreSimulator
|
||||||
|
{
|
||||||
|
public int AccuracyScore { get; private set; }
|
||||||
|
|
||||||
|
public int ComboScore { get; private set; }
|
||||||
|
|
||||||
|
public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore;
|
||||||
|
|
||||||
|
private int legacyBonusScore;
|
||||||
|
private int modernBonusScore;
|
||||||
|
private int combo;
|
||||||
|
|
||||||
|
private double modMultiplier;
|
||||||
|
private int difficultyPeppyStars;
|
||||||
|
private IBeatmap playableBeatmap = null!;
|
||||||
|
private IReadOnlyList<Mod> mods = null!;
|
||||||
|
|
||||||
|
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
||||||
|
{
|
||||||
|
this.playableBeatmap = playableBeatmap;
|
||||||
|
this.mods = mods;
|
||||||
|
|
||||||
|
IBeatmap baseBeatmap = workingBeatmap.Beatmap;
|
||||||
|
|
||||||
|
int countNormal = 0;
|
||||||
|
int countSlider = 0;
|
||||||
|
int countSpinner = 0;
|
||||||
|
|
||||||
|
foreach (HitObject obj in baseBeatmap.HitObjects)
|
||||||
|
{
|
||||||
|
switch (obj)
|
||||||
|
{
|
||||||
|
case IHasPath:
|
||||||
|
countSlider++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case IHasDuration:
|
||||||
|
countSpinner++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
countNormal++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int objectCount = countNormal + countSlider + countSpinner;
|
||||||
|
|
||||||
|
int drainLength = 0;
|
||||||
|
|
||||||
|
if (baseBeatmap.HitObjects.Count > 0)
|
||||||
|
{
|
||||||
|
int breakLength = baseBeatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum();
|
||||||
|
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
difficultyPeppyStars = (int)Math.Round(
|
||||||
|
(baseBeatmap.Difficulty.DrainRate
|
||||||
|
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||||
|
+ baseBeatmap.Difficulty.CircleSize
|
||||||
|
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||||
|
|
||||||
|
modMultiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
||||||
|
|
||||||
|
foreach (var obj in playableBeatmap.HitObjects)
|
||||||
|
simulateHit(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void simulateHit(HitObject hitObject)
|
||||||
|
{
|
||||||
|
bool increaseCombo = true;
|
||||||
|
bool addScoreComboMultiplier = false;
|
||||||
|
|
||||||
|
bool isBonus = false;
|
||||||
|
HitResult bonusResult = HitResult.None;
|
||||||
|
|
||||||
|
int scoreIncrease = 0;
|
||||||
|
|
||||||
|
switch (hitObject)
|
||||||
|
{
|
||||||
|
case SwellTick:
|
||||||
|
scoreIncrease = 300;
|
||||||
|
increaseCombo = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DrumRollTick:
|
||||||
|
scoreIncrease = 300;
|
||||||
|
increaseCombo = false;
|
||||||
|
isBonus = true;
|
||||||
|
bonusResult = HitResult.SmallBonus;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Swell swell:
|
||||||
|
// The taiko swell generally does not match the osu-stable implementation in any way.
|
||||||
|
// We'll redo the calculations to match osu-stable here...
|
||||||
|
double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(playableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5);
|
||||||
|
double secondsDuration = swell.Duration / 1000;
|
||||||
|
|
||||||
|
// The amount of half spins that are required to successfully complete the spinner (i.e. get a 300).
|
||||||
|
int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond);
|
||||||
|
|
||||||
|
halfSpinsRequiredForCompletion = (int)Math.Max(1, halfSpinsRequiredForCompletion * 1.65f);
|
||||||
|
|
||||||
|
if (mods.Any(m => m is ModDoubleTime))
|
||||||
|
halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 0.75f));
|
||||||
|
if (mods.Any(m => m is ModHalfTime))
|
||||||
|
halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 1.5f));
|
||||||
|
|
||||||
|
for (int i = 0; i <= halfSpinsRequiredForCompletion; i++)
|
||||||
|
simulateHit(new SwellTick());
|
||||||
|
|
||||||
|
scoreIncrease = 300;
|
||||||
|
addScoreComboMultiplier = true;
|
||||||
|
increaseCombo = false;
|
||||||
|
isBonus = true;
|
||||||
|
bonusResult = HitResult.LargeBonus;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Hit:
|
||||||
|
scoreIncrease = 300;
|
||||||
|
addScoreComboMultiplier = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DrumRoll:
|
||||||
|
foreach (var nested in hitObject.NestedHitObjects)
|
||||||
|
simulateHit(nested);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hitObject is DrumRollTick tick)
|
||||||
|
{
|
||||||
|
if (playableBeatmap.ControlPointInfo.EffectPointAt(tick.Parent.StartTime).KiaiMode)
|
||||||
|
scoreIncrease = (int)(scoreIncrease * 1.2f);
|
||||||
|
|
||||||
|
if (tick.IsStrong)
|
||||||
|
scoreIncrease += scoreIncrease / 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The score increase directly contributed to by the combo-multiplied portion.
|
||||||
|
int comboScoreIncrease = 0;
|
||||||
|
|
||||||
|
if (addScoreComboMultiplier)
|
||||||
|
{
|
||||||
|
int oldScoreIncrease = scoreIncrease;
|
||||||
|
|
||||||
|
// ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...)
|
||||||
|
scoreIncrease += (int)(scoreIncrease / 35 * 2 * (difficultyPeppyStars + 1) * modMultiplier) * (Math.Min(100, combo) / 10);
|
||||||
|
|
||||||
|
if (hitObject is Swell)
|
||||||
|
{
|
||||||
|
if (playableBeatmap.ControlPointInfo.EffectPointAt(hitObject.GetEndTime()).KiaiMode)
|
||||||
|
scoreIncrease = (int)(scoreIncrease * 1.2f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (playableBeatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode)
|
||||||
|
scoreIncrease = (int)(scoreIncrease * 1.2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
comboScoreIncrease = scoreIncrease - oldScoreIncrease;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hitObject is Swell || (hitObject is TaikoStrongableHitObject strongable && strongable.IsStrong))
|
||||||
|
{
|
||||||
|
scoreIncrease *= 2;
|
||||||
|
comboScoreIncrease *= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
scoreIncrease -= comboScoreIncrease;
|
||||||
|
|
||||||
|
if (addScoreComboMultiplier)
|
||||||
|
ComboScore += comboScoreIncrease;
|
||||||
|
|
||||||
|
if (isBonus)
|
||||||
|
{
|
||||||
|
legacyBonusScore += scoreIncrease;
|
||||||
|
modernBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
AccuracyScore += scoreIncrease;
|
||||||
|
|
||||||
|
if (increaseCombo)
|
||||||
|
combo++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss;
|
effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss;
|
||||||
|
|
||||||
// TODO: The detection of rulesets is temporary until the leftover old skills have been reworked.
|
// TODO: The detection of rulesets is temporary until the leftover old skills have been reworked.
|
||||||
bool isConvert = score.BeatmapInfo.Ruleset.OnlineID != 1;
|
bool isConvert = score.BeatmapInfo!.Ruleset.OnlineID != 1;
|
||||||
|
|
||||||
double multiplier = 1.13;
|
double multiplier = 1.13;
|
||||||
|
|
||||||
|
@ -4,14 +4,12 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Audio;
|
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||||
@ -93,40 +91,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
? new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit)
|
? new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit)
|
||||||
: new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
|
: new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
|
||||||
|
|
||||||
public override IEnumerable<HitSampleInfo> GetSamples()
|
|
||||||
{
|
|
||||||
// normal and claps are always handled by the drum (see DrumSampleMapping).
|
|
||||||
// in addition, whistles are excluded as they are an alternative rim marker.
|
|
||||||
|
|
||||||
var samples = HitObject.Samples.Where(s =>
|
|
||||||
s.Name != HitSampleInfo.HIT_NORMAL
|
|
||||||
&& s.Name != HitSampleInfo.HIT_CLAP
|
|
||||||
&& s.Name != HitSampleInfo.HIT_WHISTLE);
|
|
||||||
|
|
||||||
if (HitObject.Type == HitType.Rim && HitObject.IsStrong)
|
|
||||||
{
|
|
||||||
// strong + rim always maps to whistle.
|
|
||||||
// TODO: this should really be in the legacy decoder, but can't be because legacy encoding parity would be broken.
|
|
||||||
// when we add a taiko editor, this is probably not going to play nice.
|
|
||||||
|
|
||||||
var corrected = samples.ToList();
|
|
||||||
|
|
||||||
for (int i = 0; i < corrected.Count; i++)
|
|
||||||
{
|
|
||||||
var s = corrected[i];
|
|
||||||
|
|
||||||
if (s.Name != HitSampleInfo.HIT_FINISH)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
corrected[i] = s.With(HitSampleInfo.HIT_WHISTLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return corrected;
|
|
||||||
}
|
|
||||||
|
|
||||||
return samples;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
{
|
{
|
||||||
Debug.Assert(HitObject.HitWindows != null);
|
Debug.Assert(HitObject.HitWindows != null);
|
||||||
|
@ -119,8 +119,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
public override bool RemoveWhenNotAlive => false;
|
public override bool RemoveWhenNotAlive => false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Most osu!taiko hitsounds are managed by the drum (see DrumSampleTriggerSource).
|
// osu!taiko hitsounds are managed by the drum (see DrumSampleTriggerSource).
|
||||||
public override IEnumerable<HitSampleInfo> GetSamples() => Enumerable.Empty<HitSampleInfo>();
|
public sealed override IEnumerable<HitSampleInfo> GetSamples() => Enumerable.Empty<HitSampleInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract partial class DrawableTaikoHitObject<TObject> : DrawableTaikoHitObject
|
public abstract partial class DrawableTaikoHitObject<TObject> : DrawableTaikoHitObject
|
||||||
|
@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
|||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
AddNested(new DrumRollTick
|
AddNested(new DrumRollTick(this)
|
||||||
{
|
{
|
||||||
FirstTick = first,
|
FirstTick = first,
|
||||||
TickSpacing = tickSpacing,
|
TickSpacing = tickSpacing,
|
||||||
|
@ -9,6 +9,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
|||||||
{
|
{
|
||||||
public class DrumRollTick : TaikoStrongableHitObject
|
public class DrumRollTick : TaikoStrongableHitObject
|
||||||
{
|
{
|
||||||
|
public readonly DrumRoll Parent;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this is the first (initial) tick of the slider.
|
/// Whether this is the first (initial) tick of the slider.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -25,6 +27,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public double HitWindow => TickSpacing / 2;
|
public double HitWindow => TickSpacing / 2;
|
||||||
|
|
||||||
|
public DrumRollTick(DrumRoll parent)
|
||||||
|
{
|
||||||
|
Parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement();
|
public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement();
|
||||||
|
|
||||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -8,6 +9,7 @@ using osu.Framework.Graphics.Animations;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
@ -26,11 +28,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
private Bindable<int> currentCombo { get; } = new BindableInt();
|
private Bindable<int> currentCombo { get; } = new BindableInt();
|
||||||
|
|
||||||
private int animationFrame;
|
private int animationFrame;
|
||||||
private double beatLength;
|
|
||||||
|
|
||||||
// required for editor blueprints (not sure why these circle pieces are zero size).
|
// required for editor blueprints (not sure why these circle pieces are zero size).
|
||||||
public override Quad ScreenSpaceDrawQuad => backgroundLayer.ScreenSpaceDrawQuad;
|
public override Quad ScreenSpaceDrawQuad => backgroundLayer.ScreenSpaceDrawQuad;
|
||||||
|
|
||||||
|
private TimingControlPoint timingPoint = TimingControlPoint.DEFAULT;
|
||||||
|
|
||||||
public LegacyCirclePiece()
|
public LegacyCirclePiece()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
@ -39,11 +42,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private GameplayState? gameplayState { get; set; }
|
private GameplayState? gameplayState { get; set; }
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
|
||||||
private IBeatSyncProvider? beatSyncProvider { get; set; }
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(ISkinSource skin, DrawableHitObject drawableHitObject)
|
private void load(ISkinSource skin, DrawableHitObject drawableHitObject, IBeatSyncProvider? beatSyncProvider)
|
||||||
{
|
{
|
||||||
Drawable? getDrawableFor(string lookup)
|
Drawable? getDrawableFor(string lookup)
|
||||||
{
|
{
|
||||||
@ -64,6 +64,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
if (foregroundLayer != null)
|
if (foregroundLayer != null)
|
||||||
AddInternal(foregroundLayer);
|
AddInternal(foregroundLayer);
|
||||||
|
|
||||||
|
drawableHitObject.StartTimeBindable.BindValueChanged(startTime =>
|
||||||
|
{
|
||||||
|
timingPoint = beatSyncProvider?.ControlPoints?.TimingPointAt(startTime.NewValue) ?? TimingControlPoint.DEFAULT;
|
||||||
|
}, true);
|
||||||
|
|
||||||
// Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat).
|
// Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat).
|
||||||
// For now just stop at first frame for sanity.
|
// For now just stop at first frame for sanity.
|
||||||
foreach (var c in InternalChildren)
|
foreach (var c in InternalChildren)
|
||||||
@ -115,14 +120,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (beatSyncProvider?.ControlPoints != null)
|
animationFrame = Math.Abs(Time.Current - timingPoint.Time) % ((timingPoint.BeatLength * 2) / multiplier) >= timingPoint.BeatLength / multiplier ? 0 : 1;
|
||||||
{
|
animatableForegroundLayer.GotoFrame(animationFrame);
|
||||||
beatLength = beatSyncProvider.ControlPoints.TimingPointAt(Time.Current).BeatLength;
|
|
||||||
|
|
||||||
animationFrame = Time.Current % ((beatLength * 2) / multiplier) >= beatLength / multiplier ? 0 : 1;
|
|
||||||
|
|
||||||
animatableForegroundLayer.GotoFrame(animationFrame);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Color4 accentColour;
|
private Color4 accentColour;
|
||||||
|
@ -197,6 +197,8 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
|
|
||||||
public int LegacyID => 1;
|
public int LegacyID => 1;
|
||||||
|
|
||||||
|
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new TaikoLegacyScoreSimulator();
|
||||||
|
|
||||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
|
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
|
||||||
|
|
||||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TaikoRulesetConfigManager(settings, RulesetInfo);
|
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TaikoRulesetConfigManager(settings, RulesetInfo);
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
@ -18,12 +17,25 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
|
|
||||||
public void Play(HitType hitType)
|
public void Play(HitType hitType)
|
||||||
{
|
{
|
||||||
var hitSample = GetMostValidObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
|
TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject;
|
||||||
|
|
||||||
if (hitSample == null)
|
if (hitObject == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
PlaySamples(new ISampleInfo[] { new HitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL, hitSample.Bank, volume: hitSample.Volume) });
|
var baseSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL);
|
||||||
|
|
||||||
|
if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true || hitObject is StrongNestedHitObject)
|
||||||
|
{
|
||||||
|
PlaySamples(new ISampleInfo[]
|
||||||
|
{
|
||||||
|
baseSample,
|
||||||
|
hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PlaySamples(new ISampleInfo[] { baseSample });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead");
|
public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead");
|
||||||
|
@ -64,7 +64,7 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestCachedRetrievalWithFiles() => AddStep("run test", () =>
|
public void TestCachedRetrievalWithFiles() => AddStep("run test", () =>
|
||||||
{
|
{
|
||||||
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID).Detach());
|
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID)!.Detach());
|
||||||
|
|
||||||
Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0));
|
Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0));
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestForcedRefetchRetrievalWithFiles() => AddStep("run test", () =>
|
public void TestForcedRefetchRetrievalWithFiles() => AddStep("run test", () =>
|
||||||
{
|
{
|
||||||
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID).Detach());
|
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID)!.Detach());
|
||||||
|
|
||||||
Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0));
|
Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0));
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestSavePreservesCollections() => AddStep("run test", () =>
|
public void TestSavePreservesCollections() => AddStep("run test", () =>
|
||||||
{
|
{
|
||||||
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID).Detach());
|
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID)!.Detach());
|
||||||
|
|
||||||
var working = beatmaps.GetWorkingBeatmap(beatmap);
|
var working = beatmaps.GetWorkingBeatmap(beatmap);
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
return Realm.Run(r =>
|
return Realm.Run(r =>
|
||||||
{
|
{
|
||||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||||
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -51,7 +51,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
Realm.Write(r =>
|
Realm.Write(r =>
|
||||||
{
|
{
|
||||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||||
foreach (var b in beatmapSetInfo.Beatmaps)
|
foreach (var b in beatmapSetInfo.Beatmaps)
|
||||||
b.StarRating = -1;
|
b.StarRating = -1;
|
||||||
});
|
});
|
||||||
@ -66,7 +66,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
return Realm.Run(r =>
|
return Realm.Run(r =>
|
||||||
{
|
{
|
||||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||||
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -79,7 +79,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
return Realm.Run(r =>
|
return Realm.Run(r =>
|
||||||
{
|
{
|
||||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||||
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -90,7 +90,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
Realm.Write(r =>
|
Realm.Write(r =>
|
||||||
{
|
{
|
||||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||||
foreach (var b in beatmapSetInfo.Beatmaps)
|
foreach (var b in beatmapSetInfo.Beatmaps)
|
||||||
b.StarRating = -1;
|
b.StarRating = -1;
|
||||||
});
|
});
|
||||||
@ -107,7 +107,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
return Realm.Run(r =>
|
return Realm.Run(r =>
|
||||||
{
|
{
|
||||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||||
return beatmapSetInfo.Beatmaps.All(b => b.StarRating == -1);
|
return beatmapSetInfo.Beatmaps.All(b => b.StarRating == -1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -118,7 +118,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
return Realm.Run(r =>
|
return Realm.Run(r =>
|
||||||
{
|
{
|
||||||
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
|
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
|
||||||
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -18,6 +18,7 @@ using osu.Game.Extensions;
|
|||||||
using osu.Game.Models;
|
using osu.Game.Models;
|
||||||
using osu.Game.Overlays.Notifications;
|
using osu.Game.Overlays.Notifications;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using Realms;
|
using Realms;
|
||||||
using SharpCompress.Archives;
|
using SharpCompress.Archives;
|
||||||
@ -416,6 +417,108 @@ namespace osu.Game.Tests.Database
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImport_Modify_Revert()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realm, storage) =>
|
||||||
|
{
|
||||||
|
var importer = new BeatmapImporter(storage, realm);
|
||||||
|
using var store = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
|
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||||
|
|
||||||
|
await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First());
|
||||||
|
|
||||||
|
var score = realm.Run(r => r.All<ScoreInfo>().Single());
|
||||||
|
|
||||||
|
string originalHash = imported.Beatmaps.First().Hash;
|
||||||
|
const string modified_hash = "new_hash";
|
||||||
|
|
||||||
|
Assert.That(imported.Beatmaps.First().Scores.Single(), Is.EqualTo(score));
|
||||||
|
|
||||||
|
Assert.That(score.BeatmapHash, Is.EqualTo(originalHash));
|
||||||
|
Assert.That(score.BeatmapInfo, Is.EqualTo(imported.Beatmaps.First()));
|
||||||
|
|
||||||
|
// imitate making local changes via editor
|
||||||
|
// ReSharper disable once MethodHasAsyncOverload
|
||||||
|
realm.Write(r =>
|
||||||
|
{
|
||||||
|
BeatmapInfo beatmap = imported.Beatmaps.First();
|
||||||
|
beatmap.Hash = modified_hash;
|
||||||
|
beatmap.ResetOnlineInfo();
|
||||||
|
beatmap.UpdateLocalScores(r);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(!imported.Beatmaps.First().Scores.Any());
|
||||||
|
|
||||||
|
Assert.That(score.BeatmapInfo, Is.Null);
|
||||||
|
Assert.That(score.BeatmapHash, Is.EqualTo(originalHash));
|
||||||
|
|
||||||
|
// imitate reverting the local changes made above
|
||||||
|
// ReSharper disable once MethodHasAsyncOverload
|
||||||
|
realm.Write(r =>
|
||||||
|
{
|
||||||
|
BeatmapInfo beatmap = imported.Beatmaps.First();
|
||||||
|
beatmap.Hash = originalHash;
|
||||||
|
beatmap.ResetOnlineInfo();
|
||||||
|
beatmap.UpdateLocalScores(r);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(imported.Beatmaps.First().Scores.Single(), Is.EqualTo(score));
|
||||||
|
|
||||||
|
Assert.That(score.BeatmapHash, Is.EqualTo(originalHash));
|
||||||
|
Assert.That(score.BeatmapInfo, Is.EqualTo(imported.Beatmaps.First()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImport_ThenModifyMapWithScore_ThenImport()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realm, storage) =>
|
||||||
|
{
|
||||||
|
var importer = new BeatmapImporter(storage, realm);
|
||||||
|
using var store = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
|
string? temp = TestResources.GetTestBeatmapForImport();
|
||||||
|
|
||||||
|
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||||
|
|
||||||
|
await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First());
|
||||||
|
|
||||||
|
Assert.That(imported.Beatmaps.First().Scores.Any());
|
||||||
|
|
||||||
|
// imitate making local changes via editor
|
||||||
|
// ReSharper disable once MethodHasAsyncOverload
|
||||||
|
realm.Write(r =>
|
||||||
|
{
|
||||||
|
BeatmapInfo beatmap = imported.Beatmaps.First();
|
||||||
|
beatmap.Hash = "new_hash";
|
||||||
|
beatmap.ResetOnlineInfo();
|
||||||
|
beatmap.UpdateLocalScores(r);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(!imported.Beatmaps.First().Scores.Any());
|
||||||
|
|
||||||
|
var importedSecondTime = await importer.Import(new ImportTask(temp));
|
||||||
|
|
||||||
|
EnsureLoaded(realm.Realm);
|
||||||
|
|
||||||
|
// check the newly "imported" beatmap is not the original.
|
||||||
|
Assert.NotNull(importedSecondTime);
|
||||||
|
Debug.Assert(importedSecondTime != null);
|
||||||
|
Assert.That(imported.ID != importedSecondTime.ID);
|
||||||
|
|
||||||
|
var importedFirstTimeBeatmap = imported.Beatmaps.First();
|
||||||
|
var importedSecondTimeBeatmap = importedSecondTime.PerformRead(s => s.Beatmaps.First());
|
||||||
|
|
||||||
|
Assert.That(importedFirstTimeBeatmap.ID != importedSecondTimeBeatmap.ID);
|
||||||
|
Assert.That(importedFirstTimeBeatmap.Hash != importedSecondTimeBeatmap.Hash);
|
||||||
|
Assert.That(!importedFirstTimeBeatmap.Scores.Any());
|
||||||
|
Assert.That(importedSecondTimeBeatmap.Scores.Count() == 1);
|
||||||
|
Assert.That(importedSecondTimeBeatmap.Scores.Single().BeatmapInfo, Is.EqualTo(importedSecondTimeBeatmap));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestImportThenImportWithChangedFile()
|
public void TestImportThenImportWithChangedFile()
|
||||||
{
|
{
|
||||||
@ -1074,18 +1177,16 @@ namespace osu.Game.Tests.Database
|
|||||||
Assert.IsTrue(realm.All<BeatmapSetInfo>().First(_ => true).DeletePending);
|
Assert.IsTrue(realm.All<BeatmapSetInfo>().First(_ => true).DeletePending);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task createScoreForBeatmap(Realm realm, BeatmapInfo beatmap)
|
private static Task createScoreForBeatmap(Realm realm, BeatmapInfo beatmap) =>
|
||||||
{
|
realm.WriteAsync(() =>
|
||||||
// TODO: reimplement when we have score support in realm.
|
{
|
||||||
// return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
|
realm.Add(new ScoreInfo
|
||||||
// {
|
{
|
||||||
// OnlineID = 2,
|
OnlineID = 2,
|
||||||
// Beatmap = beatmap,
|
BeatmapInfo = beatmap,
|
||||||
// BeatmapInfoID = beatmap.ID
|
BeatmapHash = beatmap.Hash
|
||||||
// }, new ImportScoreTest.TestArchiveReader());
|
});
|
||||||
|
});
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void checkBeatmapSetCount(Realm realm, int expected, bool includeDeletePending = false)
|
private static void checkBeatmapSetCount(Realm realm, int expected, bool includeDeletePending = false)
|
||||||
{
|
{
|
||||||
|
@ -323,7 +323,7 @@ namespace osu.Game.Tests.Database
|
|||||||
var beatmapInfo = s.Beatmaps.First(b => b.File?.Filename != removedFilename);
|
var beatmapInfo = s.Beatmaps.First(b => b.File?.Filename != removedFilename);
|
||||||
|
|
||||||
scoreTargetBeatmapHash = beatmapInfo.Hash;
|
scoreTargetBeatmapHash = beatmapInfo.Hash;
|
||||||
s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
|
s.Realm!.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
|
||||||
});
|
});
|
||||||
|
|
||||||
realm.Run(r => r.Refresh());
|
realm.Run(r => r.Refresh());
|
||||||
@ -347,6 +347,73 @@ namespace osu.Game.Tests.Database
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDanglingScoreTransferred()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realm, storage) =>
|
||||||
|
{
|
||||||
|
var importer = new BeatmapImporter(storage, realm);
|
||||||
|
using var rulesets = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
|
using var __ = getBeatmapArchive(out string pathOriginal);
|
||||||
|
using var _ = getBeatmapArchive(out string pathOnlineCopy);
|
||||||
|
|
||||||
|
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
|
||||||
|
|
||||||
|
Assert.That(importBeforeUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importBeforeUpdate != null);
|
||||||
|
|
||||||
|
string scoreTargetBeatmapHash = string.Empty;
|
||||||
|
|
||||||
|
// set a score on the beatmap
|
||||||
|
importBeforeUpdate.PerformWrite(s =>
|
||||||
|
{
|
||||||
|
var beatmapInfo = s.Beatmaps.First();
|
||||||
|
|
||||||
|
scoreTargetBeatmapHash = beatmapInfo.Hash;
|
||||||
|
|
||||||
|
s.Realm!.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// locally modify beatmap
|
||||||
|
const string new_beatmap_hash = "new_hash";
|
||||||
|
importBeforeUpdate.PerformWrite(s =>
|
||||||
|
{
|
||||||
|
var beatmapInfo = s.Beatmaps.First(b => b.Hash == scoreTargetBeatmapHash);
|
||||||
|
|
||||||
|
beatmapInfo.Hash = new_beatmap_hash;
|
||||||
|
beatmapInfo.ResetOnlineInfo();
|
||||||
|
beatmapInfo.UpdateLocalScores(s.Realm!);
|
||||||
|
});
|
||||||
|
|
||||||
|
realm.Run(r => r.Refresh());
|
||||||
|
|
||||||
|
// making changes to a beatmap doesn't remove the score from realm, but should disassociate the beatmap.
|
||||||
|
checkCount<ScoreInfo>(realm, 1);
|
||||||
|
Assert.That(realm.Run(r => r.All<ScoreInfo>().First().BeatmapInfo), Is.Null);
|
||||||
|
|
||||||
|
// reimport the original beatmap before local modifications
|
||||||
|
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOnlineCopy), importBeforeUpdate.Value);
|
||||||
|
|
||||||
|
Assert.That(importAfterUpdate, Is.Not.Null);
|
||||||
|
Debug.Assert(importAfterUpdate != null);
|
||||||
|
|
||||||
|
realm.Run(r => r.Refresh());
|
||||||
|
|
||||||
|
// both original and locally modified versions present
|
||||||
|
checkCount<BeatmapInfo>(realm, count_beatmaps + 1);
|
||||||
|
checkCount<BeatmapMetadata>(realm, count_beatmaps + 1);
|
||||||
|
checkCount<BeatmapSetInfo>(realm, 2);
|
||||||
|
|
||||||
|
// score is preserved
|
||||||
|
checkCount<ScoreInfo>(realm, 1);
|
||||||
|
|
||||||
|
// score is transferred to new beatmap
|
||||||
|
Assert.That(importBeforeUpdate.Value.Beatmaps.First(b => b.Hash == new_beatmap_hash).Scores, Has.Count.EqualTo(0));
|
||||||
|
Assert.That(importAfterUpdate.Value.Beatmaps.First(b => b.Hash == scoreTargetBeatmapHash).Scores, Has.Count.EqualTo(1));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestScoreLostOnModification()
|
public void TestScoreLostOnModification()
|
||||||
{
|
{
|
||||||
@ -368,7 +435,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
var beatmapInfo = s.Beatmaps.Last();
|
var beatmapInfo = s.Beatmaps.Last();
|
||||||
scoreTargetFilename = beatmapInfo.File?.Filename;
|
scoreTargetFilename = beatmapInfo.File?.Filename;
|
||||||
s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
|
s.Realm!.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
|
||||||
});
|
});
|
||||||
|
|
||||||
realm.Run(r => r.Refresh());
|
realm.Run(r => r.Refresh());
|
||||||
@ -461,7 +528,7 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
importBeforeUpdate.PerformWrite(s =>
|
importBeforeUpdate.PerformWrite(s =>
|
||||||
{
|
{
|
||||||
var beatmapCollection = s.Realm.Add(new BeatmapCollection("test collection"));
|
var beatmapCollection = s.Realm!.Add(new BeatmapCollection("test collection"));
|
||||||
beatmapsToAddToCollection = s.Beatmaps.Count - (allOriginalBeatmapsInCollection ? 0 : 1);
|
beatmapsToAddToCollection = s.Beatmaps.Count - (allOriginalBeatmapsInCollection ? 0 : 1);
|
||||||
|
|
||||||
for (int i = 0; i < beatmapsToAddToCollection; i++)
|
for (int i = 0; i < beatmapsToAddToCollection; i++)
|
||||||
@ -476,7 +543,7 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
importAfterUpdate.PerformRead(updated =>
|
importAfterUpdate.PerformRead(updated =>
|
||||||
{
|
{
|
||||||
updated.Realm.Refresh();
|
updated.Realm!.Refresh();
|
||||||
|
|
||||||
string[] hashes = updated.Realm.All<BeatmapCollection>().Single().BeatmapMD5Hashes.ToArray();
|
string[] hashes = updated.Realm.All<BeatmapCollection>().Single().BeatmapMD5Hashes.ToArray();
|
||||||
|
|
||||||
@ -526,7 +593,7 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
importBeforeUpdate.PerformWrite(s =>
|
importBeforeUpdate.PerformWrite(s =>
|
||||||
{
|
{
|
||||||
var beatmapCollection = s.Realm.Add(new BeatmapCollection("test collection"));
|
var beatmapCollection = s.Realm!.Add(new BeatmapCollection("test collection"));
|
||||||
originalHash = s.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash;
|
originalHash = s.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash;
|
||||||
|
|
||||||
beatmapCollection.BeatmapMD5Hashes.Add(originalHash);
|
beatmapCollection.BeatmapMD5Hashes.Add(originalHash);
|
||||||
@ -540,7 +607,7 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
importAfterUpdate.PerformRead(updated =>
|
importAfterUpdate.PerformRead(updated =>
|
||||||
{
|
{
|
||||||
updated.Realm.Refresh();
|
updated.Realm!.Refresh();
|
||||||
|
|
||||||
string[] hashes = updated.Realm.All<BeatmapCollection>().Single().BeatmapMD5Hashes.ToArray();
|
string[] hashes = updated.Realm.All<BeatmapCollection>().Single().BeatmapMD5Hashes.ToArray();
|
||||||
string updatedHash = updated.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash;
|
string updatedHash = updated.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash;
|
||||||
|
@ -128,7 +128,7 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
realm.RegisterCustomSubscription(r =>
|
realm.RegisterCustomSubscription(r =>
|
||||||
{
|
{
|
||||||
var subscription = r.All<BeatmapInfo>().QueryAsyncWithNotifications((_, _, _) =>
|
var subscription = r.All<BeatmapInfo>().QueryAsyncWithNotifications((_, _) =>
|
||||||
{
|
{
|
||||||
realm.Run(_ =>
|
realm.Run(_ =>
|
||||||
{
|
{
|
||||||
|
@ -355,7 +355,7 @@ namespace osu.Game.Tests.Database
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
void gotChange(IRealmCollection<BeatmapInfo> sender, ChangeSet changes, Exception error)
|
void gotChange(IRealmCollection<BeatmapInfo> sender, ChangeSet? changes)
|
||||||
{
|
{
|
||||||
changesTriggered++;
|
changesTriggered++;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@ -54,7 +53,7 @@ namespace osu.Game.Tests.Database
|
|||||||
registration.Dispose();
|
registration.Dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error)
|
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
|
||||||
{
|
{
|
||||||
lastChanges = changes;
|
lastChanges = changes;
|
||||||
|
|
||||||
@ -92,7 +91,7 @@ namespace osu.Game.Tests.Database
|
|||||||
registration.Dispose();
|
registration.Dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error) => lastChanges = changes;
|
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes) => lastChanges = changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -185,7 +184,7 @@ namespace osu.Game.Tests.Database
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error)
|
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
|
||||||
{
|
{
|
||||||
if (changes == null)
|
if (changes == null)
|
||||||
resolvedItems = sender;
|
resolvedItems = sender;
|
||||||
|
@ -76,12 +76,12 @@ namespace osu.Game.Tests.Database
|
|||||||
Available = true,
|
Available = true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName).Available), Is.True);
|
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.True);
|
||||||
|
|
||||||
// Availability is updated on construction of a RealmRulesetStore
|
// Availability is updated on construction of a RealmRulesetStore
|
||||||
var _ = new RealmRulesetStore(realm, storage);
|
var _ = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName).Available), Is.False);
|
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.False);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,18 +101,18 @@ namespace osu.Game.Tests.Database
|
|||||||
Available = true,
|
Available = true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName).Available), Is.True);
|
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.True);
|
||||||
|
|
||||||
// Availability is updated on construction of a RealmRulesetStore
|
// Availability is updated on construction of a RealmRulesetStore
|
||||||
var _ = new RealmRulesetStore(realm, storage);
|
var _ = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName).Available), Is.False);
|
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.False);
|
||||||
|
|
||||||
// Simulate the ruleset getting updated
|
// Simulate the ruleset getting updated
|
||||||
LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION;
|
LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION;
|
||||||
var __ = new RealmRulesetStore(realm, storage);
|
var __ = new RealmRulesetStore(realm, storage);
|
||||||
|
|
||||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName).Available), Is.True);
|
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.True);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
realm.Run(innerRealm =>
|
realm.Run(innerRealm =>
|
||||||
{
|
{
|
||||||
var binding = innerRealm.ResolveReference(tsr);
|
var binding = innerRealm.ResolveReference(tsr)!;
|
||||||
innerRealm.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace));
|
innerRealm.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
@ -80,7 +81,9 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
{
|
{
|
||||||
TestLifetimeEntry entry = null;
|
TestLifetimeEntry entry = null;
|
||||||
AddStep("Create entry", () => entry = new TestLifetimeEntry(new HitObject()) { LifetimeStart = 1 });
|
AddStep("Create entry", () => entry = new TestLifetimeEntry(new HitObject()) { LifetimeStart = 1 });
|
||||||
|
assertJudged(() => entry, false);
|
||||||
AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()));
|
AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()));
|
||||||
|
assertJudged(() => entry, false);
|
||||||
AddAssert("Lifetime is updated", () => entry.LifetimeStart == -TestLifetimeEntry.INITIAL_LIFETIME_OFFSET);
|
AddAssert("Lifetime is updated", () => entry.LifetimeStart == -TestLifetimeEntry.INITIAL_LIFETIME_OFFSET);
|
||||||
|
|
||||||
TestDrawableHitObject dho = null;
|
TestDrawableHitObject dho = null;
|
||||||
@ -91,6 +94,7 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
});
|
});
|
||||||
AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()));
|
AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()));
|
||||||
AddAssert("Lifetime is correct", () => dho.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY && entry.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY);
|
AddAssert("Lifetime is correct", () => dho.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY && entry.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY);
|
||||||
|
assertJudged(() => entry, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -138,6 +142,29 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
AddAssert("DHO state is correct", () => dho.State.Value == ArmedState.Miss);
|
AddAssert("DHO state is correct", () => dho.State.Value == ArmedState.Miss);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestJudgedStateThroughLifetime()
|
||||||
|
{
|
||||||
|
TestDrawableHitObject dho = null;
|
||||||
|
HitObjectLifetimeEntry lifetimeEntry = null;
|
||||||
|
|
||||||
|
AddStep("Create lifetime entry", () => lifetimeEntry = new HitObjectLifetimeEntry(new HitObject { StartTime = Time.Current }));
|
||||||
|
|
||||||
|
assertJudged(() => lifetimeEntry, false);
|
||||||
|
|
||||||
|
AddStep("Create DHO and apply entry", () =>
|
||||||
|
{
|
||||||
|
Child = dho = new TestDrawableHitObject();
|
||||||
|
dho.Apply(lifetimeEntry);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertJudged(() => lifetimeEntry, false);
|
||||||
|
|
||||||
|
AddStep("Apply result", () => dho.MissForcefully());
|
||||||
|
|
||||||
|
assertJudged(() => lifetimeEntry, true);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestResultSetBeforeLoadComplete()
|
public void TestResultSetBeforeLoadComplete()
|
||||||
{
|
{
|
||||||
@ -154,15 +181,20 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
assertJudged(() => lifetimeEntry, true);
|
||||||
AddStep("Create DHO and apply entry", () =>
|
AddStep("Create DHO and apply entry", () =>
|
||||||
{
|
{
|
||||||
dho = new TestDrawableHitObject();
|
dho = new TestDrawableHitObject();
|
||||||
dho.Apply(lifetimeEntry);
|
dho.Apply(lifetimeEntry);
|
||||||
Child = dho;
|
Child = dho;
|
||||||
});
|
});
|
||||||
|
assertJudged(() => lifetimeEntry, true);
|
||||||
AddAssert("DHO state is correct", () => dho.State.Value, () => Is.EqualTo(ArmedState.Hit));
|
AddAssert("DHO state is correct", () => dho.State.Value, () => Is.EqualTo(ArmedState.Hit));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void assertJudged(Func<HitObjectLifetimeEntry> entry, bool val) =>
|
||||||
|
AddAssert(val ? "Is judged" : "Not judged", () => entry().Judged, () => Is.EqualTo(val));
|
||||||
|
|
||||||
private partial class TestDrawableHitObject : DrawableHitObject
|
private partial class TestDrawableHitObject : DrawableHitObject
|
||||||
{
|
{
|
||||||
public const double INITIAL_LIFETIME_OFFSET = 100;
|
public const double INITIAL_LIFETIME_OFFSET = 100;
|
||||||
|
@ -87,10 +87,10 @@ namespace osu.Game.Tests.Models
|
|||||||
var mock = new Mock<IScoreInfo>();
|
var mock = new Mock<IScoreInfo>();
|
||||||
|
|
||||||
mock.Setup(m => m.User).Returns(new APIUser { Username = "user" }); // TODO: temporary.
|
mock.Setup(m => m.User).Returns(new APIUser { Username = "user" }); // TODO: temporary.
|
||||||
mock.Setup(m => m.Beatmap.Metadata.Artist).Returns("artist");
|
mock.Setup(m => m.Beatmap!.Metadata.Artist).Returns("artist");
|
||||||
mock.Setup(m => m.Beatmap.Metadata.Title).Returns("title");
|
mock.Setup(m => m.Beatmap!.Metadata.Title).Returns("title");
|
||||||
mock.Setup(m => m.Beatmap.Metadata.Author.Username).Returns("author");
|
mock.Setup(m => m.Beatmap!.Metadata.Author.Username).Returns("author");
|
||||||
mock.Setup(m => m.Beatmap.DifficultyName).Returns("difficulty");
|
mock.Setup(m => m.Beatmap!.DifficultyName).Returns("difficulty");
|
||||||
|
|
||||||
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("user playing artist - title (author) [difficulty]"));
|
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("user playing artist - title (author) [difficulty]"));
|
||||||
}
|
}
|
||||||
|
@ -131,7 +131,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddAssert("results screen score has matching", () => (Player.GetChildScreen() as ResultsScreen)?.Score.Mods.First(), () => Is.EqualTo(playerMods.First()));
|
AddAssert("results screen score has matching", () => (Player.GetChildScreen() as ResultsScreen)?.Score.Mods.First(), () => Is.EqualTo(playerMods.First()));
|
||||||
|
|
||||||
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
|
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
|
||||||
AddUntilStep("databased score has correct mods", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID)).Mods.First(), () => Is.EqualTo(playerMods.First()));
|
AddUntilStep("databased score has correct mods", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID))!.Mods.First(), () => Is.EqualTo(playerMods.First()));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -170,7 +170,16 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
ManualClock clock = null;
|
ManualClock clock = null;
|
||||||
|
|
||||||
var beatmap = new Beatmap();
|
var beatmap = new Beatmap();
|
||||||
beatmap.HitObjects.Add(new TestHitObjectWithNested { Duration = 40 });
|
beatmap.HitObjects.Add(new TestHitObjectWithNested
|
||||||
|
{
|
||||||
|
Duration = 40,
|
||||||
|
NestedObjects = new HitObject[]
|
||||||
|
{
|
||||||
|
new PooledNestedHitObject { StartTime = 10 },
|
||||||
|
new PooledNestedHitObject { StartTime = 20 },
|
||||||
|
new PooledNestedHitObject { StartTime = 30 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock()));
|
createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock()));
|
||||||
|
|
||||||
@ -209,6 +218,49 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddAssert("object judged", () => playfield.JudgedObjects.Count == 1);
|
AddAssert("object judged", () => playfield.JudgedObjects.Count == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPooledObjectWithNonPooledNesteds()
|
||||||
|
{
|
||||||
|
ManualClock clock = null;
|
||||||
|
TestHitObjectWithNested hitObjectWithNested;
|
||||||
|
|
||||||
|
var beatmap = new Beatmap();
|
||||||
|
beatmap.HitObjects.Add(hitObjectWithNested = new TestHitObjectWithNested
|
||||||
|
{
|
||||||
|
Duration = 40,
|
||||||
|
NestedObjects = new HitObject[]
|
||||||
|
{
|
||||||
|
new PooledNestedHitObject { StartTime = 10 },
|
||||||
|
new NonPooledNestedHitObject { StartTime = 20 },
|
||||||
|
new NonPooledNestedHitObject { StartTime = 30 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock()));
|
||||||
|
|
||||||
|
AddAssert("hitobject entry has all nesteds", () => playfield.HitObjectContainer.Entries.Single().NestedEntries, () => Has.Count.EqualTo(3));
|
||||||
|
|
||||||
|
AddStep("skip to middle of object", () => clock.CurrentTime = (hitObjectWithNested.StartTime + hitObjectWithNested.GetEndTime()) / 2);
|
||||||
|
AddAssert("2 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(2));
|
||||||
|
AddAssert("entry not all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.False);
|
||||||
|
|
||||||
|
AddStep("skip to before end of object", () => clock.CurrentTime = hitObjectWithNested.GetEndTime() - 1);
|
||||||
|
AddAssert("3 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3));
|
||||||
|
AddAssert("entry not all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.False);
|
||||||
|
|
||||||
|
AddStep("removing object doesn't crash", () => playfield.Remove(hitObjectWithNested));
|
||||||
|
AddStep("clear judged", () => playfield.JudgedObjects.Clear());
|
||||||
|
|
||||||
|
AddStep("add object back", () => playfield.Add(hitObjectWithNested));
|
||||||
|
AddAssert("entry not all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.False);
|
||||||
|
|
||||||
|
AddStep("skip to long past object", () => clock.CurrentTime = 100_000);
|
||||||
|
// the parent entry should still be linked to nested entries of pooled objects that are managed externally
|
||||||
|
// but not contain synthetic entries that were created for the non-pooled objects.
|
||||||
|
AddAssert("entry still has non-synthetic nested entries", () => playfield.HitObjectContainer.Entries.Single().NestedEntries, () => Has.Count.EqualTo(1));
|
||||||
|
AddAssert("entry all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
private void createTest(IBeatmap beatmap, int poolSize, Func<IFrameBasedClock> createClock = null)
|
private void createTest(IBeatmap beatmap, int poolSize, Func<IFrameBasedClock> createClock = null)
|
||||||
{
|
{
|
||||||
AddStep("create test", () =>
|
AddStep("create test", () =>
|
||||||
@ -289,7 +341,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
RegisterPool<TestHitObject, DrawableTestHitObject>(poolSize);
|
RegisterPool<TestHitObject, DrawableTestHitObject>(poolSize);
|
||||||
RegisterPool<TestKilledHitObject, DrawableTestKilledHitObject>(poolSize);
|
RegisterPool<TestKilledHitObject, DrawableTestKilledHitObject>(poolSize);
|
||||||
RegisterPool<TestHitObjectWithNested, DrawableTestHitObjectWithNested>(poolSize);
|
RegisterPool<TestHitObjectWithNested, DrawableTestHitObjectWithNested>(poolSize);
|
||||||
RegisterPool<NestedHitObject, DrawableNestedHitObject>(poolSize);
|
RegisterPool<PooledNestedHitObject, DrawableNestedHitObject>(poolSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject);
|
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject);
|
||||||
@ -422,16 +474,22 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
private class TestHitObjectWithNested : TestHitObject
|
private class TestHitObjectWithNested : TestHitObject
|
||||||
{
|
{
|
||||||
|
public IEnumerable<HitObject> NestedObjects { get; init; } = Array.Empty<HitObject>();
|
||||||
|
|
||||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
base.CreateNestedHitObjects(cancellationToken);
|
base.CreateNestedHitObjects(cancellationToken);
|
||||||
|
|
||||||
for (int i = 0; i < 3; ++i)
|
foreach (var ho in NestedObjects)
|
||||||
AddNested(new NestedHitObject { StartTime = (float)Duration * (i + 1) / 4 });
|
AddNested(ho);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class NestedHitObject : ConvertHitObject
|
private class PooledNestedHitObject : ConvertHitObject
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private class NonPooledNestedHitObject : ConvertHitObject
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -482,6 +540,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
nestedContainer.Clear(false);
|
nestedContainer.Clear(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
|
||||||
|
=> hitObject is NonPooledNestedHitObject nonPooled ? new DrawableNestedHitObject(nonPooled) : null;
|
||||||
|
|
||||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||||
{
|
{
|
||||||
base.CheckForResult(userTriggered, timeOffset);
|
base.CheckForResult(userTriggered, timeOffset);
|
||||||
@ -490,25 +551,30 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private partial class DrawableNestedHitObject : DrawableHitObject<NestedHitObject>
|
private partial class DrawableNestedHitObject : DrawableHitObject
|
||||||
{
|
{
|
||||||
public DrawableNestedHitObject()
|
public DrawableNestedHitObject()
|
||||||
: this(null)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public DrawableNestedHitObject(NestedHitObject hitObject)
|
public DrawableNestedHitObject(PooledNestedHitObject hitObject)
|
||||||
|
: base(hitObject)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DrawableNestedHitObject(NonPooledNestedHitObject hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
{
|
{
|
||||||
Size = new Vector2(15);
|
|
||||||
Colour = Colour4.White;
|
|
||||||
RelativePositionAxes = Axes.Both;
|
|
||||||
Origin = Anchor.Centre;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
|
Size = new Vector2(15);
|
||||||
|
Colour = Colour4.White;
|
||||||
|
RelativePositionAxes = Axes.Both;
|
||||||
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
AddInternal(new Circle
|
AddInternal(new Circle
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
@ -107,6 +107,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestBeatmapDownloadingStates()
|
public void TestBeatmapDownloadingStates()
|
||||||
{
|
{
|
||||||
|
AddStep("set to unknown", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Unknown()));
|
||||||
AddStep("set to no map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded()));
|
AddStep("set to no map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded()));
|
||||||
AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
|
AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
|
||||||
|
|
||||||
@ -382,6 +383,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("wait for list to load", () => participantsList?.IsLoaded == true);
|
AddUntilStep("wait for list to load", () => participantsList?.IsLoaded == true);
|
||||||
|
|
||||||
|
AddStep("set beatmap available", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkProgressBarVisibility(bool visible) =>
|
private void checkProgressBarVisibility(bool visible) =>
|
||||||
|
@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
AddStep("show excess mods score", () =>
|
AddStep("show excess mods score", () =>
|
||||||
{
|
{
|
||||||
var score = TestResources.CreateTestScoreInfo();
|
var score = TestResources.CreateTestScoreInfo();
|
||||||
score.Mods = score.BeatmapInfo.Ruleset.CreateInstance().CreateAllMods().ToArray();
|
score.Mods = score.BeatmapInfo!.Ruleset.CreateInstance().CreateAllMods().ToArray();
|
||||||
showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), score);
|
showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), score);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
var author = new RealmUser { Username = "mapper_name" };
|
var author = new RealmUser { Username = "mapper_name" };
|
||||||
|
|
||||||
var score = TestResources.CreateTestScoreInfo(createTestBeatmap(author));
|
var score = TestResources.CreateTestScoreInfo(createTestBeatmap(author));
|
||||||
score.Mods = score.BeatmapInfo.Ruleset.CreateInstance().CreateAllMods().ToArray();
|
score.Mods = score.BeatmapInfo!.Ruleset.CreateInstance().CreateAllMods().ToArray();
|
||||||
|
|
||||||
showPanel(score);
|
showPanel(score);
|
||||||
});
|
});
|
||||||
|
@ -405,7 +405,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
public UnrankedSoloResultsScreen(ScoreInfo score)
|
public UnrankedSoloResultsScreen(ScoreInfo score)
|
||||||
: base(score, true)
|
: base(score, true)
|
||||||
{
|
{
|
||||||
Score.BeatmapInfo.OnlineID = 0;
|
Score.BeatmapInfo!.OnlineID = 0;
|
||||||
Score.BeatmapInfo.Status = BeatmapOnlineStatus.Pending;
|
Score.BeatmapInfo.Status = BeatmapOnlineStatus.Pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,7 +163,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
InputManager.Key(Key.Enter);
|
InputManager.Key(Key.Enter);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
|
waitForDismissed();
|
||||||
AddAssert("ensure selection changed", () => selected != Beatmap.Value);
|
AddAssert("ensure selection changed", () => selected != Beatmap.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,7 +186,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
InputManager.Key(Key.Down);
|
InputManager.Key(Key.Down);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
|
waitForDismissed();
|
||||||
AddAssert("ensure selection didn't change", () => selected == Beatmap.Value);
|
AddAssert("ensure selection didn't change", () => selected == Beatmap.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
InputManager.Key(Key.Enter);
|
InputManager.Key(Key.Enter);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
|
waitForDismissed();
|
||||||
AddAssert("ensure selection changed", () => selected != Beatmap.Value);
|
AddAssert("ensure selection changed", () => selected != Beatmap.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,7 +244,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
InputManager.ReleaseButton(MouseButton.Left);
|
InputManager.ReleaseButton(MouseButton.Left);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
|
waitForDismissed();
|
||||||
AddAssert("ensure selection didn't change", () => selected == Beatmap.Value);
|
AddAssert("ensure selection didn't change", () => selected == Beatmap.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,7 +257,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
createSongSelect();
|
createSongSelect();
|
||||||
|
|
||||||
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
|
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
|
||||||
AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
|
waitForDismissed();
|
||||||
|
|
||||||
AddStep("return", () => songSelect!.MakeCurrent());
|
AddStep("return", () => songSelect!.MakeCurrent());
|
||||||
AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen());
|
AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen());
|
||||||
@ -275,7 +275,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
createSongSelect();
|
createSongSelect();
|
||||||
|
|
||||||
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
|
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
|
||||||
AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
|
waitForDismissed();
|
||||||
|
|
||||||
AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
|
AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
|
||||||
|
|
||||||
@ -292,7 +292,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
createSongSelect();
|
createSongSelect();
|
||||||
|
|
||||||
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
|
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
|
||||||
AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
|
waitForDismissed();
|
||||||
|
|
||||||
AddStep("update beatmap", () =>
|
AddStep("update beatmap", () =>
|
||||||
{
|
{
|
||||||
@ -1011,7 +1011,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("wait for results screen presented", () => !songSelect!.IsCurrentScreen());
|
waitForDismissed();
|
||||||
|
|
||||||
AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(getPresentBeatmap()));
|
AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(getPresentBeatmap()));
|
||||||
AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0);
|
AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0);
|
||||||
@ -1040,7 +1040,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
songSelect!.PresentScore(TestResources.CreateTestScoreInfo(getPresentBeatmap()));
|
songSelect!.PresentScore(TestResources.CreateTestScoreInfo(getPresentBeatmap()));
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("wait for results screen presented", () => !songSelect!.IsCurrentScreen());
|
waitForDismissed();
|
||||||
|
|
||||||
AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(getPresentBeatmap()));
|
AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(getPresentBeatmap()));
|
||||||
AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0);
|
AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0);
|
||||||
@ -1161,6 +1161,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
rulesets.Dispose();
|
rulesets.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void waitForDismissed() => AddUntilStep("wait for not current", () => !songSelect.AsNonNull().IsCurrentScreen());
|
||||||
|
|
||||||
private partial class TestSongSelect : PlaySongSelect
|
private partial class TestSongSelect : PlaySongSelect
|
||||||
{
|
{
|
||||||
public Action? StartRequested;
|
public Action? StartRequested;
|
||||||
|
@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
|
|
||||||
var testPresets = createTestPresets();
|
var testPresets = createTestPresets();
|
||||||
foreach (var preset in testPresets)
|
foreach (var preset in testPresets)
|
||||||
preset.Ruleset = realm.Find<RulesetInfo>(preset.Ruleset.ShortName);
|
preset.Ruleset = realm.Find<RulesetInfo>(preset.Ruleset.ShortName)!;
|
||||||
|
|
||||||
realm.Add(testPresets);
|
realm.Add(testPresets);
|
||||||
});
|
});
|
||||||
@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
new ManiaModNightcore(),
|
new ManiaModNightcore(),
|
||||||
new ManiaModHardRock()
|
new ManiaModHardRock()
|
||||||
},
|
},
|
||||||
Ruleset = r.Find<RulesetInfo>("mania")
|
Ruleset = r.Find<RulesetInfo>("mania")!
|
||||||
})));
|
})));
|
||||||
AddUntilStep("2 panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 2);
|
AddUntilStep("2 panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 2);
|
||||||
|
|
||||||
@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
new OsuModHidden(),
|
new OsuModHidden(),
|
||||||
new OsuModHardRock()
|
new OsuModHardRock()
|
||||||
},
|
},
|
||||||
Ruleset = r.Find<RulesetInfo>("osu")
|
Ruleset = r.Find<RulesetInfo>("osu")!
|
||||||
})));
|
})));
|
||||||
AddUntilStep("2 panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 2);
|
AddUntilStep("2 panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 2);
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
{
|
{
|
||||||
Name = "AR0",
|
Name = "AR0",
|
||||||
Description = "Too... many... circles...",
|
Description = "Too... many... circles...",
|
||||||
Ruleset = r.Find<RulesetInfo>(OsuRuleset.SHORT_NAME),
|
Ruleset = r.Find<RulesetInfo>(OsuRuleset.SHORT_NAME)!,
|
||||||
Mods = new[]
|
Mods = new[]
|
||||||
{
|
{
|
||||||
new OsuModDifficultyAdjust
|
new OsuModDifficultyAdjust
|
||||||
@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
{
|
{
|
||||||
Name = "Half Time 0.5x",
|
Name = "Half Time 0.5x",
|
||||||
Description = "Very slow",
|
Description = "Very slow",
|
||||||
Ruleset = r.Find<RulesetInfo>(OsuRuleset.SHORT_NAME),
|
Ruleset = r.Find<RulesetInfo>(OsuRuleset.SHORT_NAME)!,
|
||||||
Mods = new[]
|
Mods = new[]
|
||||||
{
|
{
|
||||||
new OsuModHalfTime
|
new OsuModHalfTime
|
||||||
|
@ -9,7 +9,6 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
@ -94,7 +93,7 @@ namespace osu.Game.Tournament
|
|||||||
Task.Run(readBracket);
|
Task.Run(readBracket);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readBracket()
|
private async Task readBracket()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -102,7 +101,7 @@ namespace osu.Game.Tournament
|
|||||||
{
|
{
|
||||||
using (Stream stream = storage.GetStream(BRACKET_FILENAME, FileAccess.Read, FileMode.Open))
|
using (Stream stream = storage.GetStream(BRACKET_FILENAME, FileAccess.Read, FileMode.Open))
|
||||||
using (var sr = new StreamReader(stream))
|
using (var sr = new StreamReader(stream))
|
||||||
ladder = JsonConvert.DeserializeObject<LadderInfo>(sr.ReadToEnd(), new JsonPointConverter());
|
ladder = JsonConvert.DeserializeObject<LadderInfo>(await sr.ReadToEndAsync().ConfigureAwait(false), new JsonPointConverter());
|
||||||
}
|
}
|
||||||
|
|
||||||
ladder ??= new LadderInfo();
|
ladder ??= new LadderInfo();
|
||||||
@ -166,8 +165,8 @@ namespace osu.Game.Tournament
|
|||||||
}
|
}
|
||||||
|
|
||||||
addedInfo |= addPlayers();
|
addedInfo |= addPlayers();
|
||||||
addedInfo |= addRoundBeatmaps();
|
addedInfo |= await addRoundBeatmaps().ConfigureAwait(false);
|
||||||
addedInfo |= addSeedingBeatmaps();
|
addedInfo |= await addSeedingBeatmaps().ConfigureAwait(false);
|
||||||
|
|
||||||
if (addedInfo)
|
if (addedInfo)
|
||||||
saveChanges();
|
saveChanges();
|
||||||
@ -233,7 +232,7 @@ namespace osu.Game.Tournament
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add missing beatmap info based on beatmap IDs
|
/// Add missing beatmap info based on beatmap IDs
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool addRoundBeatmaps()
|
private async Task<bool> addRoundBeatmaps()
|
||||||
{
|
{
|
||||||
var beatmapsRequiringPopulation = ladder.Rounds
|
var beatmapsRequiringPopulation = ladder.Rounds
|
||||||
.SelectMany(r => r.Beatmaps)
|
.SelectMany(r => r.Beatmaps)
|
||||||
@ -246,7 +245,7 @@ namespace osu.Game.Tournament
|
|||||||
{
|
{
|
||||||
var b = beatmapsRequiringPopulation[i];
|
var b = beatmapsRequiringPopulation[i];
|
||||||
|
|
||||||
b.Beatmap = new TournamentBeatmap(beatmapCache.GetBeatmapAsync(b.ID).GetResultSafely() ?? new APIBeatmap());
|
b.Beatmap = new TournamentBeatmap(await beatmapCache.GetBeatmapAsync(b.ID).ConfigureAwait(false) ?? new APIBeatmap());
|
||||||
|
|
||||||
updateLoadProgressMessage($"Populating round beatmaps ({i} / {beatmapsRequiringPopulation.Count})");
|
updateLoadProgressMessage($"Populating round beatmaps ({i} / {beatmapsRequiringPopulation.Count})");
|
||||||
}
|
}
|
||||||
@ -257,7 +256,7 @@ namespace osu.Game.Tournament
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add missing beatmap info based on beatmap IDs
|
/// Add missing beatmap info based on beatmap IDs
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool addSeedingBeatmaps()
|
private async Task<bool> addSeedingBeatmaps()
|
||||||
{
|
{
|
||||||
var beatmapsRequiringPopulation = ladder.Teams
|
var beatmapsRequiringPopulation = ladder.Teams
|
||||||
.SelectMany(r => r.SeedingResults)
|
.SelectMany(r => r.SeedingResults)
|
||||||
@ -271,7 +270,7 @@ namespace osu.Game.Tournament
|
|||||||
{
|
{
|
||||||
var b = beatmapsRequiringPopulation[i];
|
var b = beatmapsRequiringPopulation[i];
|
||||||
|
|
||||||
b.Beatmap = new TournamentBeatmap(beatmapCache.GetBeatmapAsync(b.ID).GetResultSafely() ?? new APIBeatmap());
|
b.Beatmap = new TournamentBeatmap(await beatmapCache.GetBeatmapAsync(b.ID).ConfigureAwait(false) ?? new APIBeatmap());
|
||||||
|
|
||||||
updateLoadProgressMessage($"Populating seeding beatmaps ({i} / {beatmapsRequiringPopulation.Count})");
|
updateLoadProgressMessage($"Populating seeding beatmaps ({i} / {beatmapsRequiringPopulation.Count})");
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,11 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Scoring.Legacy;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
|
|
||||||
namespace osu.Game
|
namespace osu.Game
|
||||||
@ -25,6 +28,9 @@ namespace osu.Game
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private RulesetStore rulesetStore { get; set; } = null!;
|
private RulesetStore rulesetStore { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private BeatmapManager beatmapManager { get; set; } = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private ScoreManager scoreManager { get; set; } = null!;
|
private ScoreManager scoreManager { get; set; } = null!;
|
||||||
|
|
||||||
@ -40,19 +46,23 @@ namespace osu.Game
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private ILocalUserPlayInfo? localUserPlayInfo { get; set; }
|
private ILocalUserPlayInfo? localUserPlayInfo { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private INotificationOverlay? notificationOverlay { get; set; }
|
||||||
|
|
||||||
protected virtual int TimeToSleepDuringGameplay => 30000;
|
protected virtual int TimeToSleepDuringGameplay => 30000;
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
Task.Run(() =>
|
Task.Factory.StartNew(() =>
|
||||||
{
|
{
|
||||||
Logger.Log("Beginning background beatmap processing..");
|
Logger.Log("Beginning background beatmap processing..");
|
||||||
checkForOutdatedStarRatings();
|
checkForOutdatedStarRatings();
|
||||||
processBeatmapSetsWithMissingMetrics();
|
processBeatmapSetsWithMissingMetrics();
|
||||||
processScoresWithMissingStatistics();
|
processScoresWithMissingStatistics();
|
||||||
}).ContinueWith(t =>
|
convertLegacyTotalScoreToStandardised();
|
||||||
|
}, TaskCreationOptions.LongRunning).ContinueWith(t =>
|
||||||
{
|
{
|
||||||
if (t.Exception?.InnerException is ObjectDisposedException)
|
if (t.Exception?.InnerException is ObjectDisposedException)
|
||||||
{
|
{
|
||||||
@ -92,7 +102,7 @@ namespace osu.Game
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Find<RulesetInfo>(ruleset.ShortName).LastAppliedDifficultyVersion = currentVersion;
|
r.Find<RulesetInfo>(ruleset.ShortName)!.LastAppliedDifficultyVersion = currentVersion;
|
||||||
});
|
});
|
||||||
|
|
||||||
Logger.Log($"Finished resetting {countReset} beatmap sets for {ruleset.Name}");
|
Logger.Log($"Finished resetting {countReset} beatmap sets for {ruleset.Name}");
|
||||||
@ -121,11 +131,7 @@ namespace osu.Game
|
|||||||
|
|
||||||
foreach (var id in beatmapSetIds)
|
foreach (var id in beatmapSetIds)
|
||||||
{
|
{
|
||||||
while (localUserPlayInfo?.IsPlaying.Value == true)
|
sleepIfRequired();
|
||||||
{
|
|
||||||
Logger.Log("Background processing sleeping due to active gameplay...");
|
|
||||||
Thread.Sleep(TimeToSleepDuringGameplay);
|
|
||||||
}
|
|
||||||
|
|
||||||
realmAccess.Run(r =>
|
realmAccess.Run(r =>
|
||||||
{
|
{
|
||||||
@ -157,8 +163,12 @@ namespace osu.Game
|
|||||||
{
|
{
|
||||||
foreach (var score in r.All<ScoreInfo>())
|
foreach (var score in r.All<ScoreInfo>())
|
||||||
{
|
{
|
||||||
if (score.Statistics.Sum(kvp => kvp.Value) > 0 && score.MaximumStatistics.Sum(kvp => kvp.Value) == 0)
|
if (score.BeatmapInfo != null
|
||||||
|
&& score.Statistics.Sum(kvp => kvp.Value) > 0
|
||||||
|
&& score.MaximumStatistics.Sum(kvp => kvp.Value) == 0)
|
||||||
|
{
|
||||||
scoreIds.Add(score.ID);
|
scoreIds.Add(score.ID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -166,11 +176,7 @@ namespace osu.Game
|
|||||||
|
|
||||||
foreach (var id in scoreIds)
|
foreach (var id in scoreIds)
|
||||||
{
|
{
|
||||||
while (localUserPlayInfo?.IsPlaying.Value == true)
|
sleepIfRequired();
|
||||||
{
|
|
||||||
Logger.Log("Background processing sleeping due to active gameplay...");
|
|
||||||
Thread.Sleep(TimeToSleepDuringGameplay);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -182,16 +188,105 @@ namespace osu.Game
|
|||||||
// ReSharper disable once MethodHasAsyncOverload
|
// ReSharper disable once MethodHasAsyncOverload
|
||||||
realmAccess.Write(r =>
|
realmAccess.Write(r =>
|
||||||
{
|
{
|
||||||
r.Find<ScoreInfo>(id).MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics);
|
r.Find<ScoreInfo>(id)!.MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics);
|
||||||
});
|
});
|
||||||
|
|
||||||
Logger.Log($"Populated maximum statistics for score {id}");
|
Logger.Log($"Populated maximum statistics for score {id}");
|
||||||
}
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}");
|
Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void convertLegacyTotalScoreToStandardised()
|
||||||
|
{
|
||||||
|
Logger.Log("Querying for scores that need total score conversion...");
|
||||||
|
|
||||||
|
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(r.All<ScoreInfo>()
|
||||||
|
.Where(s => s.BeatmapInfo != null && s.TotalScoreVersion == 30000002)
|
||||||
|
.AsEnumerable().Select(s => s.ID)));
|
||||||
|
|
||||||
|
Logger.Log($"Found {scoreIds.Count} scores which require total score conversion.");
|
||||||
|
|
||||||
|
if (scoreIds.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ProgressNotification notification = new ProgressNotification { State = ProgressNotificationState.Active };
|
||||||
|
|
||||||
|
notificationOverlay?.Post(notification);
|
||||||
|
|
||||||
|
int processedCount = 0;
|
||||||
|
int failedCount = 0;
|
||||||
|
|
||||||
|
foreach (var id in scoreIds)
|
||||||
|
{
|
||||||
|
if (notification.State == ProgressNotificationState.Cancelled)
|
||||||
|
break;
|
||||||
|
|
||||||
|
notification.Text = $"Upgrading scores to new scoring algorithm ({processedCount} of {scoreIds.Count})";
|
||||||
|
notification.Progress = (float)processedCount / scoreIds.Count;
|
||||||
|
|
||||||
|
sleepIfRequired();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var score = scoreManager.Query(s => s.ID == id);
|
||||||
|
long newTotalScore = StandardisedScoreMigrationTools.ConvertFromLegacyTotalScore(score, beatmapManager);
|
||||||
|
|
||||||
|
// Can't use async overload because we're not on the update thread.
|
||||||
|
// ReSharper disable once MethodHasAsyncOverload
|
||||||
|
realmAccess.Write(r =>
|
||||||
|
{
|
||||||
|
ScoreInfo s = r.Find<ScoreInfo>(id)!;
|
||||||
|
s.TotalScore = newTotalScore;
|
||||||
|
s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION;
|
||||||
|
});
|
||||||
|
|
||||||
|
Logger.Log($"Converted total score for score {id}");
|
||||||
|
++processedCount;
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Log($"Failed to convert total score for {id}: {e}");
|
||||||
|
++failedCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processedCount == scoreIds.Count)
|
||||||
|
{
|
||||||
|
notification.CompletionText = $"{processedCount} score(s) have been upgraded to the new scoring algorithm";
|
||||||
|
notification.Progress = 1;
|
||||||
|
notification.State = ProgressNotificationState.Completed;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
notification.Text = $"{processedCount} of {scoreIds.Count} score(s) have been upgraded to the new scoring algorithm.";
|
||||||
|
|
||||||
|
// We may have arrived here due to user cancellation or completion with failures.
|
||||||
|
if (failedCount > 0)
|
||||||
|
notification.Text += $" Check logs for issues with {failedCount} failed upgrades.";
|
||||||
|
|
||||||
|
notification.State = ProgressNotificationState.Cancelled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sleepIfRequired()
|
||||||
|
{
|
||||||
|
while (localUserPlayInfo?.IsPlaying.Value == true)
|
||||||
|
{
|
||||||
|
Logger.Log("Background processing sleeping due to active gameplay...");
|
||||||
|
Thread.Sleep(TimeToSleepDuringGameplay);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
Logger.Log($"Beatmap \"{updated}\" update completed successfully", LoggingTarget.Database);
|
Logger.Log($"Beatmap \"{updated}\" update completed successfully", LoggingTarget.Database);
|
||||||
|
|
||||||
original = realm.Find<BeatmapSetInfo>(original.ID);
|
original = realm!.Find<BeatmapSetInfo>(original.ID)!;
|
||||||
|
|
||||||
// Generally the import process will do this for us if the OnlineIDs match,
|
// Generally the import process will do this for us if the OnlineIDs match,
|
||||||
// but that isn't a guarantee (ie. if the .osu file doesn't have OnlineIDs populated).
|
// but that isn't a guarantee (ie. if the .osu file doesn't have OnlineIDs populated).
|
||||||
@ -204,6 +204,14 @@ namespace osu.Game.Beatmaps
|
|||||||
protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters parameters)
|
protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters parameters)
|
||||||
{
|
{
|
||||||
base.PostImport(model, realm, parameters);
|
base.PostImport(model, realm, parameters);
|
||||||
|
|
||||||
|
// Scores are stored separately from beatmaps, and persist even when a beatmap is modified or deleted.
|
||||||
|
// Let's reattach any matching scores that exist in the database, based on hash.
|
||||||
|
foreach (BeatmapInfo beatmap in model.Beatmaps)
|
||||||
|
{
|
||||||
|
beatmap.UpdateLocalScores(realm);
|
||||||
|
}
|
||||||
|
|
||||||
ProcessBeatmap?.Invoke(model, parameters.Batch ? MetadataLookupScope.LocalCacheFirst : MetadataLookupScope.OnlineFirst);
|
ProcessBeatmap?.Invoke(model, parameters.Batch ? MetadataLookupScope.LocalCacheFirst : MetadataLookupScope.OnlineFirst);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,6 +234,22 @@ namespace osu.Game.Beatmaps
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Local scores are retained separate from a beatmap's lifetime, matched via <see cref="ScoreInfo.BeatmapHash"/>.
|
||||||
|
/// Therefore we need to detach / reattach scores when a beatmap is edited or imported.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="realm">A realm instance in an active write transaction.</param>
|
||||||
|
public void UpdateLocalScores(Realm realm)
|
||||||
|
{
|
||||||
|
// first disassociate any scores which are already attached and no longer valid.
|
||||||
|
foreach (var score in Scores)
|
||||||
|
score.BeatmapInfo = null;
|
||||||
|
|
||||||
|
// then attach any scores which match the new hash.
|
||||||
|
foreach (var score in realm.All<ScoreInfo>().Where(s => s.BeatmapHash == Hash))
|
||||||
|
score.BeatmapInfo = this;
|
||||||
|
}
|
||||||
|
|
||||||
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
|
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
|
||||||
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
|
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
|
||||||
IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;
|
IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;
|
||||||
|
@ -208,7 +208,7 @@ namespace osu.Game.Beatmaps
|
|||||||
using (var transaction = r.BeginWrite())
|
using (var transaction = r.BeginWrite())
|
||||||
{
|
{
|
||||||
if (!beatmapInfo.IsManaged)
|
if (!beatmapInfo.IsManaged)
|
||||||
beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID);
|
beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID)!;
|
||||||
|
|
||||||
beatmapInfo.Hidden = true;
|
beatmapInfo.Hidden = true;
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
@ -227,7 +227,7 @@ namespace osu.Game.Beatmaps
|
|||||||
using (var transaction = r.BeginWrite())
|
using (var transaction = r.BeginWrite())
|
||||||
{
|
{
|
||||||
if (!beatmapInfo.IsManaged)
|
if (!beatmapInfo.IsManaged)
|
||||||
beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID);
|
beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID)!;
|
||||||
|
|
||||||
beatmapInfo.Hidden = false;
|
beatmapInfo.Hidden = false;
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
@ -330,7 +330,7 @@ namespace osu.Game.Beatmaps
|
|||||||
Realm.Write(r =>
|
Realm.Write(r =>
|
||||||
{
|
{
|
||||||
if (!beatmapInfo.IsManaged)
|
if (!beatmapInfo.IsManaged)
|
||||||
beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID);
|
beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID)!;
|
||||||
|
|
||||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||||
Debug.Assert(beatmapInfo.File != null);
|
Debug.Assert(beatmapInfo.File != null);
|
||||||
@ -460,13 +460,16 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
Realm.Write(r =>
|
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)
|
if (transferCollections)
|
||||||
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
|
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
|
||||||
|
|
||||||
|
liveBeatmapSet.Beatmaps.Single(b => b.ID == beatmapInfo.ID)
|
||||||
|
.UpdateLocalScores(r);
|
||||||
|
|
||||||
// do not look up metadata.
|
// 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.
|
// 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);
|
ProcessBeatmap?.Invoke(liveBeatmapSet, MetadataLookupScope.None);
|
||||||
|
@ -52,7 +52,7 @@ namespace osu.Game.Beatmaps
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="beatmapSet">The managed beatmap set to update. A transaction will be opened to apply changes.</param>
|
/// <param name="beatmapSet">The managed beatmap set to update. A transaction will be opened to apply changes.</param>
|
||||||
/// <param name="lookupScope">The preferred scope to use for metadata lookup.</param>
|
/// <param name="lookupScope">The preferred scope to use for metadata lookup.</param>
|
||||||
public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapSet.Realm.Write(r =>
|
public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapSet.Realm!.Write(_ =>
|
||||||
{
|
{
|
||||||
// Before we use below, we want to invalidate.
|
// Before we use below, we want to invalidate.
|
||||||
workingBeatmapCache.Invalidate(beatmapSet);
|
workingBeatmapCache.Invalidate(beatmapSet);
|
||||||
|
@ -59,7 +59,7 @@ namespace osu.Game.Collections
|
|||||||
Current.BindValueChanged(selectionChanged);
|
Current.BindValueChanged(selectionChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void collectionsChanged(IRealmCollection<BeatmapCollection> collections, ChangeSet? changes, Exception error)
|
private void collectionsChanged(IRealmCollection<BeatmapCollection> collections, ChangeSet? changes)
|
||||||
{
|
{
|
||||||
var selectedItem = SelectedItem?.Value?.Collection;
|
var selectedItem = SelectedItem?.Value?.Collection;
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Collections
|
|||||||
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapCollection>().OrderBy(c => c.Name), collectionsChanged);
|
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapCollection>().OrderBy(c => c.Name), collectionsChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void collectionsChanged(IRealmCollection<BeatmapCollection> collections, ChangeSet? changes, Exception error)
|
private void collectionsChanged(IRealmCollection<BeatmapCollection> collections, ChangeSet? changes)
|
||||||
{
|
{
|
||||||
Items.Clear();
|
Items.Clear();
|
||||||
Items.AddRange(collections.AsEnumerable().Select(c => c.ToLive(realm)));
|
Items.AddRange(collections.AsEnumerable().Select(c => c.ToLive(realm)));
|
||||||
|
@ -197,7 +197,7 @@ namespace osu.Game.Collections
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteCollection() => collection.PerformWrite(c => c.Realm.Remove(c));
|
private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,6 +129,7 @@ namespace osu.Game.Configuration
|
|||||||
SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true);
|
SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true);
|
||||||
SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true);
|
SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true);
|
||||||
SetDefault(OsuSetting.KeyOverlay, false);
|
SetDefault(OsuSetting.KeyOverlay, false);
|
||||||
|
SetDefault(OsuSetting.ReplaySettingsOverlay, true);
|
||||||
SetDefault(OsuSetting.GameplayLeaderboard, true);
|
SetDefault(OsuSetting.GameplayLeaderboard, true);
|
||||||
SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true);
|
SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true);
|
||||||
|
|
||||||
@ -382,6 +383,7 @@ namespace osu.Game.Configuration
|
|||||||
SafeAreaConsiderations,
|
SafeAreaConsiderations,
|
||||||
ComboColourNormalisationAmount,
|
ComboColourNormalisationAmount,
|
||||||
ProfileCoverExpanded,
|
ProfileCoverExpanded,
|
||||||
EditorLimitedDistanceSnap
|
EditorLimitedDistanceSnap,
|
||||||
|
ReplaySettingsOverlay
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,8 @@ namespace osu.Game.Database
|
|||||||
IEnumerator IEnumerable.GetEnumerator() => emptySet.GetEnumerator();
|
IEnumerator IEnumerable.GetEnumerator() => emptySet.GetEnumerator();
|
||||||
public int Count => emptySet.Count;
|
public int Count => emptySet.Count;
|
||||||
public T this[int index] => emptySet[index];
|
public T this[int index] => emptySet[index];
|
||||||
public int IndexOf(object item) => emptySet.IndexOf((T)item);
|
public int IndexOf(object? item) => item == null ? -1 : emptySet.IndexOf((T)item);
|
||||||
public bool Contains(object item) => emptySet.Contains((T)item);
|
public bool Contains(object? item) => item != null && emptySet.Contains((T)item);
|
||||||
|
|
||||||
public event NotifyCollectionChangedEventHandler? CollectionChanged
|
public event NotifyCollectionChangedEventHandler? CollectionChanged
|
||||||
{
|
{
|
||||||
|
@ -34,13 +34,13 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteFile(TModel item, RealmNamedFileUsage file) =>
|
public void DeleteFile(TModel item, RealmNamedFileUsage file) =>
|
||||||
performFileOperation(item, managed => DeleteFile(managed, managed.Files.First(f => f.Filename == file.Filename), managed.Realm));
|
performFileOperation(item, managed => DeleteFile(managed, managed.Files.First(f => f.Filename == file.Filename), managed.Realm!));
|
||||||
|
|
||||||
public void ReplaceFile(TModel item, RealmNamedFileUsage file, Stream contents) =>
|
public void ReplaceFile(TModel item, RealmNamedFileUsage file, Stream contents) =>
|
||||||
performFileOperation(item, managed => ReplaceFile(file, contents, managed.Realm));
|
performFileOperation(item, managed => ReplaceFile(file, contents, managed.Realm!));
|
||||||
|
|
||||||
public void AddFile(TModel item, Stream contents, string filename) =>
|
public void AddFile(TModel item, Stream contents, string filename) =>
|
||||||
performFileOperation(item, managed => AddFile(managed, contents, filename, managed.Realm));
|
performFileOperation(item, managed => AddFile(managed, contents, filename, managed.Realm!));
|
||||||
|
|
||||||
private void performFileOperation(TModel item, Action<TModel> operation)
|
private void performFileOperation(TModel item, Action<TModel> operation)
|
||||||
{
|
{
|
||||||
@ -178,13 +178,14 @@ namespace osu.Game.Database
|
|||||||
// (ie. if an async import finished very recently).
|
// (ie. if an async import finished very recently).
|
||||||
return Realm.Write(realm =>
|
return Realm.Write(realm =>
|
||||||
{
|
{
|
||||||
if (!item.IsManaged)
|
TModel? processableItem = item;
|
||||||
item = realm.Find<TModel>(item.ID);
|
if (!processableItem.IsManaged)
|
||||||
|
processableItem = realm.Find<TModel>(item.ID);
|
||||||
|
|
||||||
if (item?.DeletePending != false)
|
if (processableItem?.DeletePending != false)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
item.DeletePending = true;
|
processableItem.DeletePending = true;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -195,13 +196,14 @@ namespace osu.Game.Database
|
|||||||
// (ie. if an async import finished very recently).
|
// (ie. if an async import finished very recently).
|
||||||
Realm.Write(realm =>
|
Realm.Write(realm =>
|
||||||
{
|
{
|
||||||
if (!item.IsManaged)
|
TModel? processableItem = item;
|
||||||
item = realm.Find<TModel>(item.ID);
|
if (!processableItem.IsManaged)
|
||||||
|
processableItem = realm.Find<TModel>(item.ID);
|
||||||
|
|
||||||
if (item?.DeletePending != true)
|
if (processableItem?.DeletePending != true)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
item.DeletePending = false;
|
processableItem.DeletePending = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,8 +78,9 @@ namespace osu.Game.Database
|
|||||||
/// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files.
|
/// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files.
|
||||||
/// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes.
|
/// 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.
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int schema_version = 30;
|
private const int schema_version = 31;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||||
@ -534,7 +535,7 @@ namespace osu.Game.Database
|
|||||||
lock (notificationsResetMap)
|
lock (notificationsResetMap)
|
||||||
{
|
{
|
||||||
// Store an action which is used when blocking to ensure consumers don't use results of a stale changeset firing.
|
// Store an action which is used when blocking to ensure consumers don't use results of a stale changeset firing.
|
||||||
notificationsResetMap.Add(action, () => callback(new EmptyRealmSet<T>(), null, null));
|
notificationsResetMap.Add(action, () => callback(new EmptyRealmSet<T>(), null));
|
||||||
}
|
}
|
||||||
|
|
||||||
return RegisterCustomSubscription(action);
|
return RegisterCustomSubscription(action);
|
||||||
@ -754,10 +755,10 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
for (int i = 0; i < itemCount; i++)
|
for (int i = 0; i < itemCount; i++)
|
||||||
{
|
{
|
||||||
dynamic? oldItem = oldItems.ElementAt(i);
|
dynamic oldItem = oldItems.ElementAt(i);
|
||||||
dynamic? newItem = newItems.ElementAt(i);
|
dynamic newItem = newItems.ElementAt(i);
|
||||||
|
|
||||||
long? nullableOnlineID = oldItem?.OnlineID;
|
long? nullableOnlineID = oldItem.OnlineID;
|
||||||
newItem.OnlineID = (int)(nullableOnlineID ?? -1);
|
newItem.OnlineID = (int)(nullableOnlineID ?? -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -794,7 +795,7 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
for (int i = 0; i < metadataCount; i++)
|
for (int i = 0; i < metadataCount; i++)
|
||||||
{
|
{
|
||||||
dynamic? oldItem = oldMetadata.ElementAt(i);
|
dynamic oldItem = oldMetadata.ElementAt(i);
|
||||||
var newItem = newMetadata.ElementAt(i);
|
var newItem = newMetadata.ElementAt(i);
|
||||||
|
|
||||||
string username = oldItem.Author;
|
string username = oldItem.Author;
|
||||||
@ -817,7 +818,7 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
for (int i = 0; i < newSettings.Count; i++)
|
for (int i = 0; i < newSettings.Count; i++)
|
||||||
{
|
{
|
||||||
dynamic? oldItem = oldSettings.ElementAt(i);
|
dynamic oldItem = oldSettings.ElementAt(i);
|
||||||
var newItem = newSettings.ElementAt(i);
|
var newItem = newSettings.ElementAt(i);
|
||||||
|
|
||||||
long rulesetId = oldItem.RulesetID;
|
long rulesetId = oldItem.RulesetID;
|
||||||
@ -842,7 +843,7 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
for (int i = 0; i < newKeyBindings.Count; i++)
|
for (int i = 0; i < newKeyBindings.Count; i++)
|
||||||
{
|
{
|
||||||
dynamic? oldItem = oldKeyBindings.ElementAt(i);
|
dynamic oldItem = oldKeyBindings.ElementAt(i);
|
||||||
var newItem = newKeyBindings.ElementAt(i);
|
var newItem = newKeyBindings.ElementAt(i);
|
||||||
|
|
||||||
if (oldItem.RulesetID == null)
|
if (oldItem.RulesetID == null)
|
||||||
@ -896,7 +897,7 @@ namespace osu.Game.Database
|
|||||||
var scores = migration.NewRealm.All<ScoreInfo>();
|
var scores = migration.NewRealm.All<ScoreInfo>();
|
||||||
|
|
||||||
foreach (var score in scores)
|
foreach (var score in scores)
|
||||||
score.BeatmapHash = score.BeatmapInfo.Hash;
|
score.BeatmapHash = score.BeatmapInfo?.Hash ?? string.Empty;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -966,6 +967,25 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 31:
|
||||||
|
{
|
||||||
|
foreach (var score in migration.NewRealm.All<ScoreInfo>())
|
||||||
|
{
|
||||||
|
if (score.IsLegacyScore && score.Ruleset.IsLegacyRuleset())
|
||||||
|
{
|
||||||
|
// Scores with this version will trigger the score upgrade process in BackgroundBeatmapProcessor.
|
||||||
|
score.TotalScoreVersion = 30000002;
|
||||||
|
|
||||||
|
// Transfer known legacy scores to a permanent storage field for preservation.
|
||||||
|
score.LegacyTotalScore = score.TotalScore;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
score.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
|
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using osu.Framework.Development;
|
using osu.Framework.Development;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Statistics;
|
using osu.Framework.Statistics;
|
||||||
using Realms;
|
using Realms;
|
||||||
|
|
||||||
@ -104,7 +105,7 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
PerformRead(t =>
|
PerformRead(t =>
|
||||||
{
|
{
|
||||||
using (var transaction = t.Realm.BeginWrite())
|
using (var transaction = t.Realm!.BeginWrite())
|
||||||
{
|
{
|
||||||
perform(t);
|
perform(t);
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
@ -133,7 +134,7 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
Debug.Assert(ThreadSafety.IsUpdateThread);
|
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||||
|
|
||||||
if (dataIsFromUpdateThread && !data.Realm.IsClosed)
|
if (dataIsFromUpdateThread && !data.Realm.AsNonNull().IsClosed)
|
||||||
{
|
{
|
||||||
RealmLiveStatistics.USAGE_UPDATE_IMMEDIATE.Value++;
|
RealmLiveStatistics.USAGE_UPDATE_IMMEDIATE.Value++;
|
||||||
return;
|
return;
|
||||||
@ -154,7 +155,7 @@ namespace osu.Game.Database
|
|||||||
// To ensure that behaviour matches what we'd expect (the object *is* available), force
|
// To ensure that behaviour matches what we'd expect (the object *is* available), force
|
||||||
// a refresh to bring in any off-thread changes immediately.
|
// a refresh to bring in any off-thread changes immediately.
|
||||||
realm.Refresh();
|
realm.Refresh();
|
||||||
found = realm.Find<T>(ID);
|
found = realm.Find<T>(ID)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
return found;
|
return found;
|
||||||
|
@ -43,7 +43,7 @@ namespace osu.Game.Database
|
|||||||
.ForMember(s => s.BeatmapSet, cc => cc.Ignore())
|
.ForMember(s => s.BeatmapSet, cc => cc.Ignore())
|
||||||
.AfterMap((s, d) =>
|
.AfterMap((s, d) =>
|
||||||
{
|
{
|
||||||
d.Ruleset = d.Realm.Find<RulesetInfo>(s.Ruleset.ShortName);
|
d.Ruleset = d.Realm!.Find<RulesetInfo>(s.Ruleset.ShortName)!;
|
||||||
copyChangesToRealm(s.Difficulty, d.Difficulty);
|
copyChangesToRealm(s.Difficulty, d.Difficulty);
|
||||||
copyChangesToRealm(s.Metadata, d.Metadata);
|
copyChangesToRealm(s.Metadata, d.Metadata);
|
||||||
});
|
});
|
||||||
@ -57,7 +57,7 @@ namespace osu.Game.Database
|
|||||||
// Importantly, search all of realm for the beatmap (not just the set's beatmaps).
|
// Importantly, search all of realm for the beatmap (not just the set's beatmaps).
|
||||||
// It may have gotten detached, and if that's the case let's use this opportunity to fix
|
// It may have gotten detached, and if that's the case let's use this opportunity to fix
|
||||||
// things up.
|
// things up.
|
||||||
var existingBeatmap = d.Realm.Find<BeatmapInfo>(beatmap.ID);
|
var existingBeatmap = d.Realm!.Find<BeatmapInfo>(beatmap.ID);
|
||||||
|
|
||||||
if (existingBeatmap != null)
|
if (existingBeatmap != null)
|
||||||
{
|
{
|
||||||
@ -77,7 +77,7 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
ID = beatmap.ID,
|
ID = beatmap.ID,
|
||||||
BeatmapSet = d,
|
BeatmapSet = d,
|
||||||
Ruleset = d.Realm.Find<RulesetInfo>(beatmap.Ruleset.ShortName)
|
Ruleset = d.Realm.Find<RulesetInfo>(beatmap.Ruleset.ShortName)!
|
||||||
};
|
};
|
||||||
|
|
||||||
d.Beatmaps.Add(newBeatmap);
|
d.Beatmaps.Add(newBeatmap);
|
||||||
@ -282,12 +282,10 @@ namespace osu.Game.Database
|
|||||||
/// <returns>
|
/// <returns>
|
||||||
/// A subscription token. It must be kept alive for as long as you want to receive change notifications.
|
/// A subscription token. It must be kept alive for as long as you want to receive change notifications.
|
||||||
/// To stop receiving notifications, call <see cref="M:System.IDisposable.Dispose" />.
|
/// To stop receiving notifications, call <see cref="M:System.IDisposable.Dispose" />.
|
||||||
///
|
|
||||||
/// May be null in the case the provided collection is not managed.
|
|
||||||
/// </returns>
|
/// </returns>
|
||||||
/// <seealso cref="M:Realms.CollectionExtensions.SubscribeForNotifications``1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0})" />
|
/// <seealso cref="M:Realms.CollectionExtensions.SubscribeForNotifications``1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0})" />
|
||||||
/// <seealso cref="M:Realms.CollectionExtensions.SubscribeForNotifications``1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0})" />
|
/// <seealso cref="M:Realms.CollectionExtensions.SubscribeForNotifications``1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0})" />
|
||||||
public static IDisposable? QueryAsyncWithNotifications<T>(this IRealmCollection<T> collection, NotificationCallbackDelegate<T> callback)
|
public static IDisposable QueryAsyncWithNotifications<T>(this IRealmCollection<T> collection, NotificationCallbackDelegate<T> callback)
|
||||||
where T : RealmObjectBase
|
where T : RealmObjectBase
|
||||||
{
|
{
|
||||||
if (!RealmAccess.CurrentThreadSubscriptionsAllowed)
|
if (!RealmAccess.CurrentThreadSubscriptionsAllowed)
|
||||||
|
@ -3,8 +3,11 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
@ -185,6 +188,100 @@ namespace osu.Game.Database
|
|||||||
return (long)Math.Round((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier);
|
return (long)Math.Round((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts from <see cref="ScoreInfo.LegacyTotalScore"/> to the new standardised scoring of <see cref="ScoreProcessor"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="score">The score to convert the total score of.</param>
|
||||||
|
/// <param name="beatmaps">A <see cref="BeatmapManager"/> used for <see cref="WorkingBeatmap"/> lookups.</param>
|
||||||
|
/// <returns>The standardised total score.</returns>
|
||||||
|
public static long ConvertFromLegacyTotalScore(ScoreInfo score, BeatmapManager beatmaps)
|
||||||
|
{
|
||||||
|
if (!score.IsLegacyScore)
|
||||||
|
return score.TotalScore;
|
||||||
|
|
||||||
|
WorkingBeatmap beatmap = beatmaps.GetWorkingBeatmap(score.BeatmapInfo);
|
||||||
|
Ruleset ruleset = score.Ruleset.CreateInstance();
|
||||||
|
|
||||||
|
if (ruleset is not ILegacyRuleset legacyRuleset)
|
||||||
|
return score.TotalScore;
|
||||||
|
|
||||||
|
var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods);
|
||||||
|
|
||||||
|
if (playableBeatmap.HitObjects.Count == 0)
|
||||||
|
throw new InvalidOperationException("Beatmap contains no hit objects!");
|
||||||
|
|
||||||
|
ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator();
|
||||||
|
|
||||||
|
sv1Simulator.Simulate(beatmap, playableBeatmap, score.Mods);
|
||||||
|
|
||||||
|
return ConvertFromLegacyTotalScore(score, new DifficultyAttributes
|
||||||
|
{
|
||||||
|
LegacyAccuracyScore = sv1Simulator.AccuracyScore,
|
||||||
|
LegacyComboScore = sv1Simulator.ComboScore,
|
||||||
|
LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts from <see cref="ScoreInfo.LegacyTotalScore"/> to the new standardised scoring of <see cref="ScoreProcessor"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="score">The score to convert the total score of.</param>
|
||||||
|
/// <param name="attributes">Difficulty attributes providing the legacy scoring values
|
||||||
|
/// (<see cref="DifficultyAttributes.LegacyAccuracyScore"/>, <see cref="DifficultyAttributes.LegacyComboScore"/>, and <see cref="DifficultyAttributes.LegacyBonusScoreRatio"/>)
|
||||||
|
/// for the beatmap which the score was set on.</param>
|
||||||
|
/// <returns>The standardised total score.</returns>
|
||||||
|
public static long ConvertFromLegacyTotalScore(ScoreInfo score, DifficultyAttributes attributes)
|
||||||
|
{
|
||||||
|
if (!score.IsLegacyScore)
|
||||||
|
return score.TotalScore;
|
||||||
|
|
||||||
|
Debug.Assert(score.LegacyTotalScore != null);
|
||||||
|
|
||||||
|
int maximumLegacyAccuracyScore = attributes.LegacyAccuracyScore;
|
||||||
|
int maximumLegacyComboScore = attributes.LegacyComboScore;
|
||||||
|
double maximumLegacyBonusRatio = attributes.LegacyBonusScoreRatio;
|
||||||
|
double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n);
|
||||||
|
|
||||||
|
// The part of total score that doesn't include bonus.
|
||||||
|
int maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore;
|
||||||
|
|
||||||
|
// The combo proportion is calculated as a proportion of maximumLegacyBaseScore.
|
||||||
|
double comboProportion = Math.Min(1, (double)score.LegacyTotalScore / maximumLegacyBaseScore);
|
||||||
|
|
||||||
|
// The bonus proportion makes up the rest of the score that exceeds maximumLegacyBaseScore.
|
||||||
|
double bonusProportion = Math.Max(0, ((long)score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio);
|
||||||
|
|
||||||
|
switch (score.Ruleset.OnlineID)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
return (long)Math.Round((
|
||||||
|
700000 * comboProportion
|
||||||
|
+ 300000 * Math.Pow(score.Accuracy, 10)
|
||||||
|
+ bonusProportion) * modMultiplier);
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
return (long)Math.Round((
|
||||||
|
250000 * comboProportion
|
||||||
|
+ 750000 * Math.Pow(score.Accuracy, 3.6)
|
||||||
|
+ bonusProportion) * modMultiplier);
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
return (long)Math.Round((
|
||||||
|
600000 * comboProportion
|
||||||
|
+ 400000 * score.Accuracy
|
||||||
|
+ bonusProportion) * modMultiplier);
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
return (long)Math.Round((
|
||||||
|
990000 * comboProportion
|
||||||
|
+ 10000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy)
|
||||||
|
+ bonusProportion) * modMultiplier);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return score.TotalScore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class FakeHit : HitObject
|
private class FakeHit : HitObject
|
||||||
{
|
{
|
||||||
private readonly Judgement judgement;
|
private readonly Judgement judgement;
|
||||||
|
@ -51,7 +51,7 @@ namespace osu.Game.Input.Bindings
|
|||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
realmSubscription = realm.RegisterForNotifications(queryRealmKeyBindings, (sender, _, _) =>
|
realmSubscription = realm.RegisterForNotifications(queryRealmKeyBindings, (sender, _) =>
|
||||||
{
|
{
|
||||||
// The first fire of this is a bit redundant as this is being called in base.LoadComplete,
|
// The first fire of this is a bit redundant as this is being called in base.LoadComplete,
|
||||||
// but this is safest in case the subscription is restored after a context recycle.
|
// but this is safest in case the subscription is restored after a context recycle.
|
||||||
|
@ -129,6 +129,7 @@ namespace osu.Game.Input.Bindings
|
|||||||
new KeyBinding(InputKey.MouseMiddle, GlobalAction.TogglePauseReplay),
|
new KeyBinding(InputKey.MouseMiddle, GlobalAction.TogglePauseReplay),
|
||||||
new KeyBinding(InputKey.Left, GlobalAction.SeekReplayBackward),
|
new KeyBinding(InputKey.Left, GlobalAction.SeekReplayBackward),
|
||||||
new KeyBinding(InputKey.Right, GlobalAction.SeekReplayForward),
|
new KeyBinding(InputKey.Right, GlobalAction.SeekReplayForward),
|
||||||
|
new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.ToggleReplaySettings),
|
||||||
};
|
};
|
||||||
|
|
||||||
public IEnumerable<KeyBinding> SongSelectKeyBindings => new[]
|
public IEnumerable<KeyBinding> SongSelectKeyBindings => new[]
|
||||||
@ -374,5 +375,8 @@ namespace osu.Game.Input.Bindings
|
|||||||
|
|
||||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ExportReplay))]
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ExportReplay))]
|
||||||
ExportReplay,
|
ExportReplay,
|
||||||
|
|
||||||
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleReplaySettings))]
|
||||||
|
ToggleReplaySettings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,10 +65,15 @@ namespace osu.Game.Localisation
|
|||||||
public static LocalisableString HUDVisibilityMode => new TranslatableString(getKey(@"hud_visibility_mode"), @"HUD overlay visibility mode");
|
public static LocalisableString HUDVisibilityMode => new TranslatableString(getKey(@"hud_visibility_mode"), @"HUD overlay visibility mode");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Show health display even when you can't fail"
|
/// "Show health display even when you can't fail"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString ShowHealthDisplayWhenCantFail => new TranslatableString(getKey(@"show_health_display_when_cant_fail"), @"Show health display even when you can't fail");
|
public static LocalisableString ShowHealthDisplayWhenCantFail => new TranslatableString(getKey(@"show_health_display_when_cant_fail"), @"Show health display even when you can't fail");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Show replay settings overlay"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString ShowReplaySettingsOverlay => new TranslatableString(getKey(@"show_replay_settings_overlay"), @"Show replay settings overlay");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Fade playfield to red when health is low"
|
/// "Fade playfield to red when health is low"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -134,6 +139,6 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString ClassicScoreDisplay => new TranslatableString(getKey(@"classic_score_display"), @"Classic");
|
public static LocalisableString ClassicScoreDisplay => new TranslatableString(getKey(@"classic_score_display"), @"Classic");
|
||||||
|
|
||||||
private static string getKey(string key) => $"{prefix}:{key}";
|
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -324,6 +324,11 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString ToggleChatFocus => new TranslatableString(getKey(@"toggle_chat_focus"), @"Toggle chat focus");
|
public static LocalisableString ToggleChatFocus => new TranslatableString(getKey(@"toggle_chat_focus"), @"Toggle chat focus");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Toggle replay settings"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString ToggleReplaySettings => new TranslatableString(getKey(@"toggle_replay_settings"), @"Toggle replay settings");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Save replay"
|
/// "Save replay"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -24,6 +24,11 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString SignedIn => new TranslatableString(getKey(@"signed_in"), @"Signed in");
|
public static LocalisableString SignedIn => new TranslatableString(getKey(@"signed_in"), @"Signed in");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Sign out"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString SignOut => new TranslatableString(getKey(@"sign_out"), @"Sign out");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Account"
|
/// "Account"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -40,7 +40,7 @@ namespace osu.Game.Online
|
|||||||
// Used to interact with manager classes that don't support interface types. Will eventually be replaced.
|
// Used to interact with manager classes that don't support interface types. Will eventually be replaced.
|
||||||
var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID };
|
var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID };
|
||||||
|
|
||||||
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, _, _) =>
|
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, _) =>
|
||||||
{
|
{
|
||||||
if (items.Any())
|
if (items.Any())
|
||||||
Schedule(() => UpdateState(DownloadState.LocallyAvailable));
|
Schedule(() => UpdateState(DownloadState.LocallyAvailable));
|
||||||
|
@ -171,6 +171,8 @@ namespace osu.Game.Online.Chat
|
|||||||
|
|
||||||
public abstract partial class HighlightMessageNotification : SimpleNotification
|
public abstract partial class HighlightMessageNotification : SimpleNotification
|
||||||
{
|
{
|
||||||
|
public override string PopInSampleName => "UI/notification-mention";
|
||||||
|
|
||||||
protected HighlightMessageNotification(Message message, Channel channel)
|
protected HighlightMessageNotification(Message message, Channel channel)
|
||||||
{
|
{
|
||||||
this.message = message;
|
this.message = message;
|
||||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
/// The availability state of the current beatmap.
|
/// The availability state of the current beatmap.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Key(2)]
|
[Key(2)]
|
||||||
public BeatmapAvailability BeatmapAvailability { get; set; } = BeatmapAvailability.LocallyAvailable();
|
public BeatmapAvailability BeatmapAvailability { get; set; } = BeatmapAvailability.Unknown();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Any mods applicable only to the local user.
|
/// Any mods applicable only to the local user.
|
||||||
|
@ -34,6 +34,7 @@ namespace osu.Game.Online.Rooms
|
|||||||
DownloadProgress = downloadProgress;
|
DownloadProgress = downloadProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static BeatmapAvailability Unknown() => new BeatmapAvailability(DownloadState.Unknown);
|
||||||
public static BeatmapAvailability NotDownloaded() => new BeatmapAvailability(DownloadState.NotDownloaded);
|
public static BeatmapAvailability NotDownloaded() => new BeatmapAvailability(DownloadState.NotDownloaded);
|
||||||
public static BeatmapAvailability Downloading(float progress) => new BeatmapAvailability(DownloadState.Downloading, progress);
|
public static BeatmapAvailability Downloading(float progress) => new BeatmapAvailability(DownloadState.Downloading, progress);
|
||||||
public static BeatmapAvailability Importing() => new BeatmapAvailability(DownloadState.Importing);
|
public static BeatmapAvailability Importing() => new BeatmapAvailability(DownloadState.Importing);
|
||||||
|
@ -60,6 +60,15 @@ namespace osu.Game.Online.Rooms
|
|||||||
if (item.NewValue == null)
|
if (item.NewValue == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Initially set to unknown until we have attained a good state.
|
||||||
|
// This has the wanted side effect of forcing a state change when the current playlist
|
||||||
|
// item changes at the server but our local availability doesn't necessarily change
|
||||||
|
// (ie. we have both the previous and next item LocallyAvailable).
|
||||||
|
//
|
||||||
|
// Note that even without this, the server will trigger a state change and things will work.
|
||||||
|
// This is just for safety.
|
||||||
|
availability.Value = BeatmapAvailability.Unknown();
|
||||||
|
|
||||||
downloadTracker?.RemoveAndDisposeImmediately();
|
downloadTracker?.RemoveAndDisposeImmediately();
|
||||||
selectedBeatmap = null;
|
selectedBeatmap = null;
|
||||||
|
|
||||||
@ -98,7 +107,7 @@ namespace osu.Game.Online.Rooms
|
|||||||
|
|
||||||
// handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow).
|
// handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow).
|
||||||
realmSubscription?.Dispose();
|
realmSubscription?.Dispose();
|
||||||
realmSubscription = realm.RegisterForNotifications(_ => filteredBeatmaps(), (_, changes, _) =>
|
realmSubscription = realm.RegisterForNotifications(_ => filteredBeatmaps(), (_, changes) =>
|
||||||
{
|
{
|
||||||
if (changes == null)
|
if (changes == null)
|
||||||
return;
|
return;
|
||||||
@ -115,6 +124,9 @@ namespace osu.Game.Online.Rooms
|
|||||||
switch (downloadTracker.State.Value)
|
switch (downloadTracker.State.Value)
|
||||||
{
|
{
|
||||||
case DownloadState.Unknown:
|
case DownloadState.Unknown:
|
||||||
|
availability.Value = BeatmapAvailability.Unknown();
|
||||||
|
break;
|
||||||
|
|
||||||
case DownloadState.NotDownloaded:
|
case DownloadState.NotDownloaded:
|
||||||
availability.Value = BeatmapAvailability.NotDownloaded();
|
availability.Value = BeatmapAvailability.NotDownloaded();
|
||||||
break;
|
break;
|
||||||
|
@ -48,7 +48,7 @@ namespace osu.Game.Online
|
|||||||
realmSubscription = realm.RegisterForNotifications(r => r.All<ScoreInfo>().Where(s =>
|
realmSubscription = realm.RegisterForNotifications(r => r.All<ScoreInfo>().Where(s =>
|
||||||
((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID)
|
((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID)
|
||||||
|| (!string.IsNullOrEmpty(s.Hash) && s.Hash == TrackedItem.Hash))
|
|| (!string.IsNullOrEmpty(s.Hash) && s.Hash == TrackedItem.Hash))
|
||||||
&& !s.DeletePending), (items, _, _) =>
|
&& !s.DeletePending), (items, _) =>
|
||||||
{
|
{
|
||||||
if (items.Any())
|
if (items.Any())
|
||||||
Schedule(() => UpdateState(DownloadState.LocallyAvailable));
|
Schedule(() => UpdateState(DownloadState.LocallyAvailable));
|
||||||
|
@ -185,7 +185,7 @@ namespace osu.Game.Online.Spectator
|
|||||||
IsPlaying = true;
|
IsPlaying = true;
|
||||||
|
|
||||||
// transfer state at point of beginning play
|
// transfer state at point of beginning play
|
||||||
currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID;
|
currentState.BeatmapID = score.ScoreInfo.BeatmapInfo!.OnlineID;
|
||||||
currentState.RulesetID = score.ScoreInfo.RulesetID;
|
currentState.RulesetID = score.ScoreInfo.RulesetID;
|
||||||
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
|
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
|
||||||
currentState.State = SpectatedUserState.Playing;
|
currentState.State = SpectatedUserState.Playing;
|
||||||
|
@ -163,7 +163,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
|||||||
},
|
},
|
||||||
username,
|
username,
|
||||||
#pragma warning disable 618
|
#pragma warning disable 618
|
||||||
new StatisticText(score.MaxCombo, score.BeatmapInfo.MaxCombo, @"0\x"),
|
new StatisticText(score.MaxCombo, score.BeatmapInfo!.MaxCombo, @"0\x"),
|
||||||
#pragma warning restore 618
|
#pragma warning restore 618
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -123,7 +123,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
|||||||
accuracyColumn.Text = value.DisplayAccuracy;
|
accuracyColumn.Text = value.DisplayAccuracy;
|
||||||
maxComboColumn.Text = value.MaxCombo.ToLocalisableString(@"0\x");
|
maxComboColumn.Text = value.MaxCombo.ToLocalisableString(@"0\x");
|
||||||
|
|
||||||
ppColumn.Alpha = value.BeatmapInfo.Status.GrantsPerformancePoints() ? 1 : 0;
|
ppColumn.Alpha = value.BeatmapInfo!.Status.GrantsPerformancePoints() ? 1 : 0;
|
||||||
|
|
||||||
if (value.PP is double pp)
|
if (value.PP is double pp)
|
||||||
ppColumn.Text = pp.ToLocalisableString(@"N0");
|
ppColumn.Text = pp.ToLocalisableString(@"N0");
|
||||||
|
@ -225,7 +225,12 @@ namespace osu.Game.Overlays.Dialog
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Programmatically clicks the first button of the provided type.
|
/// Programmatically clicks the first button of the provided type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void PerformAction<T>() where T : PopupDialogButton => Buttons.OfType<T>().First().TriggerClick();
|
public void PerformAction<T>() where T : PopupDialogButton
|
||||||
|
{
|
||||||
|
// Buttons are regularly added in BDL or LoadComplete, so let's schedule to ensure
|
||||||
|
// they are ready to be pressed.
|
||||||
|
Schedule(() => Buttons.OfType<T>().First().TriggerClick());
|
||||||
|
}
|
||||||
|
|
||||||
protected override bool OnKeyDown(KeyDownEvent e)
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
{
|
{
|
||||||
|
@ -123,7 +123,7 @@ namespace osu.Game.Overlays.FirstRunSetup
|
|||||||
beatmapSubscription?.Dispose();
|
beatmapSubscription?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error) => Schedule(() =>
|
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes) => Schedule(() =>
|
||||||
{
|
{
|
||||||
currentlyLoadedBeatmaps.Text = FirstRunSetupBeatmapScreenStrings.CurrentlyLoadedBeatmaps(sender.Count);
|
currentlyLoadedBeatmaps.Text = FirstRunSetupBeatmapScreenStrings.CurrentlyLoadedBeatmaps(sender.Count);
|
||||||
|
|
||||||
|
@ -44,6 +44,6 @@ namespace osu.Game.Overlays
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// All ongoing operations (ie. any <see cref="ProgressNotification"/> not in a completed state).
|
/// All ongoing operations (ie. any <see cref="ProgressNotification"/> not in a completed state).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<ProgressNotification> OngoingOperations => AllNotifications.OfType<ProgressNotification>().Where(p => p.State != ProgressNotificationState.Completed);
|
public IEnumerable<ProgressNotification> OngoingOperations => AllNotifications.OfType<ProgressNotification>().Where(p => p.State != ProgressNotificationState.Completed && p.State != ProgressNotificationState.Cancelled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Login
|
|||||||
[LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.AppearOffline))]
|
[LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.AppearOffline))]
|
||||||
AppearOffline,
|
AppearOffline,
|
||||||
|
|
||||||
[LocalisableDescription(typeof(UserVerificationStrings), nameof(UserVerificationStrings.BoxInfoLogoutLink))]
|
[LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.SignOut))]
|
||||||
SignOut,
|
SignOut,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -102,7 +102,7 @@ namespace osu.Game.Overlays.Mods
|
|||||||
Name = nameTextBox.Current.Value,
|
Name = nameTextBox.Current.Value,
|
||||||
Description = descriptionTextBox.Current.Value,
|
Description = descriptionTextBox.Current.Value,
|
||||||
Mods = selectedMods.Value.ToArray(),
|
Mods = selectedMods.Value.ToArray(),
|
||||||
Ruleset = r.Find<RulesetInfo>(ruleset.Value.ShortName)
|
Ruleset = r.Find<RulesetInfo>(ruleset.Value.ShortName)!
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.HidePopover();
|
this.HidePopover();
|
||||||
|
@ -61,7 +61,7 @@ namespace osu.Game.Overlays.Mods
|
|||||||
private Task? latestLoadTask;
|
private Task? latestLoadTask;
|
||||||
internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true;
|
internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true;
|
||||||
|
|
||||||
private void asyncLoadPanels(IRealmCollection<ModPreset> presets, ChangeSet changes, Exception error)
|
private void asyncLoadPanels(IRealmCollection<ModPreset> presets, ChangeSet? changes)
|
||||||
{
|
{
|
||||||
cancellationTokenSource?.Cancel();
|
cancellationTokenSource?.Cancel();
|
||||||
|
|
||||||
|
@ -109,7 +109,7 @@ namespace osu.Game.Overlays.Music
|
|||||||
beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo.ToLive(realm), true);
|
beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo.ToLive(realm), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet changes, Exception error)
|
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet changes)
|
||||||
{
|
{
|
||||||
if (changes == null)
|
if (changes == null)
|
||||||
{
|
{
|
||||||
|
@ -169,7 +169,7 @@ namespace osu.Game.Overlays
|
|||||||
|
|
||||||
Logger.Log($"⚠️ {notification.Text}");
|
Logger.Log($"⚠️ {notification.Text}");
|
||||||
|
|
||||||
notification.Closed += notificationClosed;
|
notification.Closed += () => notificationClosed(notification);
|
||||||
|
|
||||||
if (notification is IHasCompletionTarget hasCompletionTarget)
|
if (notification is IHasCompletionTarget hasCompletionTarget)
|
||||||
hasCompletionTarget.CompletionTarget = Post;
|
hasCompletionTarget.CompletionTarget = Post;
|
||||||
@ -229,17 +229,20 @@ namespace osu.Game.Overlays
|
|||||||
mainContent.FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.In);
|
mainContent.FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.In);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notificationClosed() => Schedule(() =>
|
private void notificationClosed(Notification notification) => Schedule(() =>
|
||||||
{
|
{
|
||||||
updateCounts();
|
updateCounts();
|
||||||
|
|
||||||
// this debounce is currently shared between popin/popout sounds, which means one could potentially not play when the user is expecting it.
|
// this debounce is currently shared between popin/popout sounds, which means one could potentially not play when the user is expecting it.
|
||||||
// popout is constant across all notification types, and should therefore be handled using playback concurrency instead, but seems broken at the moment.
|
// popout is constant across all notification types, and should therefore be handled using playback concurrency instead, but seems broken at the moment.
|
||||||
playDebouncedSample("UI/overlay-pop-out");
|
playDebouncedSample(notification.PopOutSampleName);
|
||||||
});
|
});
|
||||||
|
|
||||||
private void playDebouncedSample(string sampleName)
|
private void playDebouncedSample(string sampleName)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrEmpty(sampleName))
|
||||||
|
return;
|
||||||
|
|
||||||
if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME)
|
if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME)
|
||||||
{
|
{
|
||||||
audio.Samples.Get(sampleName)?.Play();
|
audio.Samples.Get(sampleName)?.Play();
|
||||||
|
@ -50,7 +50,8 @@ namespace osu.Game.Overlays.Notifications
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual bool DisplayOnTop => true;
|
public virtual bool DisplayOnTop => true;
|
||||||
|
|
||||||
public virtual string PopInSampleName => "UI/notification-pop-in";
|
public virtual string PopInSampleName => "UI/notification-default";
|
||||||
|
public virtual string PopOutSampleName => "UI/overlay-pop-out";
|
||||||
|
|
||||||
protected NotificationLight Light;
|
protected NotificationLight Light;
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@ namespace osu.Game.Overlays.Notifications
|
|||||||
{
|
{
|
||||||
public partial class ProgressCompletionNotification : SimpleNotification
|
public partial class ProgressCompletionNotification : SimpleNotification
|
||||||
{
|
{
|
||||||
|
public override string PopInSampleName => "UI/notification-done";
|
||||||
|
|
||||||
public ProgressCompletionNotification()
|
public ProgressCompletionNotification()
|
||||||
{
|
{
|
||||||
Icon = FontAwesome.Solid.Check;
|
Icon = FontAwesome.Solid.Check;
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Audio.Sample;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Colour;
|
using osu.Framework.Graphics.Colour;
|
||||||
@ -27,6 +29,8 @@ namespace osu.Game.Overlays.Notifications
|
|||||||
|
|
||||||
protected override bool AllowFlingDismiss => false;
|
protected override bool AllowFlingDismiss => false;
|
||||||
|
|
||||||
|
public override string PopOutSampleName => State is ProgressNotificationState.Cancelled ? base.PopOutSampleName : "";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The function to post completion notifications back to.
|
/// The function to post completion notifications back to.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -122,6 +126,7 @@ namespace osu.Game.Overlays.Notifications
|
|||||||
cancellationTokenSource.Cancel();
|
cancellationTokenSource.Cancel();
|
||||||
|
|
||||||
IconContent.FadeColour(ColourInfo.GradientVertical(Color4.Gray, Color4.Gray.Lighten(0.5f)), colour_fade_duration);
|
IconContent.FadeColour(ColourInfo.GradientVertical(Color4.Gray, Color4.Gray.Lighten(0.5f)), colour_fade_duration);
|
||||||
|
cancelSample?.Play();
|
||||||
loadingSpinner.Hide();
|
loadingSpinner.Hide();
|
||||||
|
|
||||||
var icon = new SpriteIcon
|
var icon = new SpriteIcon
|
||||||
@ -190,6 +195,8 @@ namespace osu.Game.Overlays.Notifications
|
|||||||
|
|
||||||
private LoadingSpinner loadingSpinner = null!;
|
private LoadingSpinner loadingSpinner = null!;
|
||||||
|
|
||||||
|
private Sample? cancelSample;
|
||||||
|
|
||||||
private readonly TextFlowContainer textDrawable;
|
private readonly TextFlowContainer textDrawable;
|
||||||
|
|
||||||
public ProgressNotification()
|
public ProgressNotification()
|
||||||
@ -217,7 +224,7 @@ namespace osu.Game.Overlays.Notifications
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours, AudioManager audioManager)
|
||||||
{
|
{
|
||||||
colourQueued = colours.YellowDark;
|
colourQueued = colours.YellowDark;
|
||||||
colourActive = colours.Blue;
|
colourActive = colours.Blue;
|
||||||
@ -236,6 +243,8 @@ namespace osu.Game.Overlays.Notifications
|
|||||||
Size = new Vector2(loading_spinner_size),
|
Size = new Vector2(loading_spinner_size),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cancelSample = audioManager.Samples.Get(@"UI/notification-cancel");
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Close(bool runFlingAnimation)
|
public override void Close(bool runFlingAnimation)
|
||||||
|
@ -7,7 +7,7 @@ namespace osu.Game.Overlays.Notifications
|
|||||||
{
|
{
|
||||||
public partial class SimpleErrorNotification : SimpleNotification
|
public partial class SimpleErrorNotification : SimpleNotification
|
||||||
{
|
{
|
||||||
public override string PopInSampleName => "UI/error-notification-pop-in";
|
public override string PopInSampleName => "UI/notification-error";
|
||||||
|
|
||||||
public SimpleErrorNotification()
|
public SimpleErrorNotification()
|
||||||
{
|
{
|
||||||
|
@ -25,10 +25,9 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
|
|||||||
},
|
},
|
||||||
new SettingsCheckbox
|
new SettingsCheckbox
|
||||||
{
|
{
|
||||||
ClassicDefault = false,
|
LabelText = GameplaySettingsStrings.ShowReplaySettingsOverlay,
|
||||||
LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail,
|
Current = config.GetBindable<bool>(OsuSetting.ReplaySettingsOverlay),
|
||||||
Current = config.GetBindable<bool>(OsuSetting.ShowHealthDisplayWhenCantFail),
|
Keywords = new[] { "hide" },
|
||||||
Keywords = new[] { "hp", "bar" }
|
|
||||||
},
|
},
|
||||||
new SettingsCheckbox
|
new SettingsCheckbox
|
||||||
{
|
{
|
||||||
@ -41,6 +40,13 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
|
|||||||
LabelText = GameplaySettingsStrings.AlwaysShowGameplayLeaderboard,
|
LabelText = GameplaySettingsStrings.AlwaysShowGameplayLeaderboard,
|
||||||
Current = config.GetBindable<bool>(OsuSetting.GameplayLeaderboard),
|
Current = config.GetBindable<bool>(OsuSetting.GameplayLeaderboard),
|
||||||
},
|
},
|
||||||
|
new SettingsCheckbox
|
||||||
|
{
|
||||||
|
ClassicDefault = false,
|
||||||
|
LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail,
|
||||||
|
Current = config.GetBindable<bool>(OsuSetting.ShowHealthDisplayWhenCantFail),
|
||||||
|
Keywords = new[] { "hp", "bar" }
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -440,7 +440,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateStoreFromButton(KeyButton button) =>
|
private void updateStoreFromButton(KeyButton button) =>
|
||||||
realm.WriteAsync(r => r.Find<RealmKeyBinding>(button.KeyBinding.ID).KeyCombinationString = button.KeyBinding.KeyCombinationString);
|
realm.WriteAsync(r => r.Find<RealmKeyBinding>(button.KeyBinding.ID)!.KeyCombinationString = button.KeyBinding.KeyCombinationString);
|
||||||
|
|
||||||
private void updateIsDefaultValue()
|
private void updateIsDefaultValue()
|
||||||
{
|
{
|
||||||
|
@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Settings.Sections
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void skinsChanged(IRealmCollection<SkinInfo> sender, ChangeSet changes, Exception error)
|
private void skinsChanged(IRealmCollection<SkinInfo> sender, ChangeSet changes)
|
||||||
{
|
{
|
||||||
// This can only mean that realm is recycling, else we would see the protected skins.
|
// This can only mean that realm is recycling, else we would see the protected skins.
|
||||||
// Because we are using `Live<>` in this class, we don't need to worry about this scenario too much.
|
// Because we are using `Live<>` in this class, we don't need to worry about this scenario too much.
|
||||||
|
@ -3,10 +3,10 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Difficulty
|
namespace osu.Game.Rulesets.Difficulty
|
||||||
{
|
{
|
||||||
@ -27,6 +27,9 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
protected const int ATTRIB_ID_FLASHLIGHT = 17;
|
protected const int ATTRIB_ID_FLASHLIGHT = 17;
|
||||||
protected const int ATTRIB_ID_SLIDER_FACTOR = 19;
|
protected const int ATTRIB_ID_SLIDER_FACTOR = 19;
|
||||||
protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21;
|
protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21;
|
||||||
|
protected const int ATTRIB_ID_LEGACY_ACCURACY_SCORE = 23;
|
||||||
|
protected const int ATTRIB_ID_LEGACY_COMBO_SCORE = 25;
|
||||||
|
protected const int ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO = 27;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The mods which were applied to the beatmap.
|
/// The mods which were applied to the beatmap.
|
||||||
@ -45,6 +48,22 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
[JsonProperty("max_combo", Order = -2)]
|
[JsonProperty("max_combo", Order = -2)]
|
||||||
public int MaxCombo { get; set; }
|
public int MaxCombo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The accuracy portion of the legacy (ScoreV1) total score.
|
||||||
|
/// </summary>
|
||||||
|
public int LegacyAccuracyScore { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The combo-multiplied portion of the legacy (ScoreV1) total score.
|
||||||
|
/// </summary>
|
||||||
|
public int LegacyComboScore { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A ratio of <c>new_bonus_score / old_bonus_score</c> for converting the bonus score of legacy scores to the new scoring.
|
||||||
|
/// This is made up of all judgements that would be <see cref="HitResult.SmallBonus"/> or <see cref="HitResult.LargeBonus"/>.
|
||||||
|
/// </summary>
|
||||||
|
public double LegacyBonusScoreRatio { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates new <see cref="DifficultyAttributes"/>.
|
/// Creates new <see cref="DifficultyAttributes"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -69,7 +88,13 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// See: osu_difficulty_attribs table.
|
/// See: osu_difficulty_attribs table.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() => Enumerable.Empty<(int, object)>();
|
public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
|
||||||
|
{
|
||||||
|
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
||||||
|
yield return (ATTRIB_ID_LEGACY_ACCURACY_SCORE, LegacyAccuracyScore);
|
||||||
|
yield return (ATTRIB_ID_LEGACY_COMBO_SCORE, LegacyComboScore);
|
||||||
|
yield return (ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO, LegacyBonusScoreRatio);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads osu-web database attribute mappings into this <see cref="DifficultyAttributes"/> object.
|
/// Reads osu-web database attribute mappings into this <see cref="DifficultyAttributes"/> object.
|
||||||
@ -78,6 +103,10 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
/// <param name="onlineInfo">The <see cref="IBeatmapOnlineInfo"/> where more information about the beatmap may be extracted from (such as AR/CS/OD/etc).</param>
|
/// <param name="onlineInfo">The <see cref="IBeatmapOnlineInfo"/> where more information about the beatmap may be extracted from (such as AR/CS/OD/etc).</param>
|
||||||
public virtual void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
public virtual void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||||
{
|
{
|
||||||
|
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
||||||
|
LegacyAccuracyScore = (int)values[ATTRIB_ID_LEGACY_ACCURACY_SCORE];
|
||||||
|
LegacyComboScore = (int)values[ATTRIB_ID_LEGACY_COMBO_SCORE];
|
||||||
|
LegacyBonusScoreRatio = (int)values[ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,13 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
{
|
{
|
||||||
public abstract class DifficultyCalculator
|
public abstract class DifficultyCalculator
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether legacy scoring values (ScoreV1) should be computed to populate the difficulty attributes
|
||||||
|
/// <see cref="DifficultyAttributes.LegacyAccuracyScore"/>, <see cref="DifficultyAttributes.LegacyComboScore"/>,
|
||||||
|
/// and <see cref="DifficultyAttributes.LegacyBonusScoreRatio"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool ComputeLegacyScoringValues;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The beatmap for which difficulty will be calculated.
|
/// The beatmap for which difficulty will be calculated.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets
|
namespace osu.Game.Rulesets
|
||||||
{
|
{
|
||||||
public interface ILegacyRuleset
|
public interface ILegacyRuleset
|
||||||
@ -11,5 +13,7 @@ namespace osu.Game.Rulesets
|
|||||||
/// Identifies the server-side ID of a legacy ruleset.
|
/// Identifies the server-side ID of a legacy ruleset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
int LegacyID { get; }
|
int LegacyID { get; }
|
||||||
|
|
||||||
|
ILegacyScoreSimulator CreateLegacyScoreSimulator();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
public virtual bool DisplayResult => true;
|
public virtual bool DisplayResult => true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this <see cref="DrawableHitObject"/> and all of its nested <see cref="DrawableHitObject"/>s have been judged.
|
/// The scoring result of this <see cref="DrawableHitObject"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool AllJudged => Judged && NestedHitObjects.All(h => h.AllJudged);
|
public JudgementResult Result => Entry?.Result;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this <see cref="DrawableHitObject"/> has been hit. This occurs if <see cref="Result"/> is hit.
|
/// Whether this <see cref="DrawableHitObject"/> has been hit. This occurs if <see cref="Result"/> is hit.
|
||||||
@ -112,12 +112,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
/// Whether this <see cref="DrawableHitObject"/> has been judged.
|
/// Whether this <see cref="DrawableHitObject"/> has been judged.
|
||||||
/// Note: This does NOT include nested hitobjects.
|
/// Note: This does NOT include nested hitobjects.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Judged => Result?.HasResult ?? true;
|
public bool Judged => Entry?.Judged ?? false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The scoring result of this <see cref="DrawableHitObject"/>.
|
/// Whether this <see cref="DrawableHitObject"/> and all of its nested <see cref="DrawableHitObject"/>s have been judged.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public JudgementResult Result => Entry?.Result;
|
public bool AllJudged => Entry?.AllJudged ?? false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The relative X position of this hit object for sample playback balance adjustment.
|
/// The relative X position of this hit object for sample playback balance adjustment.
|
||||||
@ -218,6 +218,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
|
|
||||||
protected sealed override void OnApply(HitObjectLifetimeEntry entry)
|
protected sealed override void OnApply(HitObjectLifetimeEntry entry)
|
||||||
{
|
{
|
||||||
|
Debug.Assert(Entry != null);
|
||||||
|
|
||||||
// LifetimeStart is already computed using HitObjectLifetimeEntry's InitialLifetimeOffset.
|
// LifetimeStart is already computed using HitObjectLifetimeEntry's InitialLifetimeOffset.
|
||||||
// We override this with DHO's InitialLifetimeOffset for a non-pooled DHO.
|
// We override this with DHO's InitialLifetimeOffset for a non-pooled DHO.
|
||||||
if (entry is SyntheticHitObjectEntry)
|
if (entry is SyntheticHitObjectEntry)
|
||||||
@ -247,6 +249,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
drawableNested.ParentHitObject = this;
|
drawableNested.ParentHitObject = this;
|
||||||
|
|
||||||
nestedHitObjects.Add(drawableNested);
|
nestedHitObjects.Add(drawableNested);
|
||||||
|
|
||||||
|
// assume that synthetic entries are not pooled and therefore need to be managed from within the DHO.
|
||||||
|
// this is important for the correctness of value of flags such as `AllJudged`.
|
||||||
|
if (drawableNested.Entry is SyntheticHitObjectEntry syntheticNestedEntry)
|
||||||
|
Entry.NestedEntries.Add(syntheticNestedEntry);
|
||||||
|
|
||||||
AddNestedHitObject(drawableNested);
|
AddNestedHitObject(drawableNested);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,6 +298,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
|
|
||||||
protected sealed override void OnFree(HitObjectLifetimeEntry entry)
|
protected sealed override void OnFree(HitObjectLifetimeEntry entry)
|
||||||
{
|
{
|
||||||
|
Debug.Assert(Entry != null);
|
||||||
|
|
||||||
StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable);
|
StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable);
|
||||||
|
|
||||||
if (HitObject is IHasComboInformation combo)
|
if (HitObject is IHasComboInformation combo)
|
||||||
@ -318,6 +328,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
}
|
}
|
||||||
|
|
||||||
nestedHitObjects.Clear();
|
nestedHitObjects.Clear();
|
||||||
|
// clean up synthetic entries manually added in `Apply()`.
|
||||||
|
Entry.NestedEntries.RemoveAll(nestedEntry => nestedEntry is SyntheticHitObjectEntry);
|
||||||
ClearNestedHitObjects();
|
ClearNestedHitObjects();
|
||||||
|
|
||||||
HitObject.DefaultsApplied -= onDefaultsApplied;
|
HitObject.DefaultsApplied -= onDefaultsApplied;
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics.Performance;
|
using osu.Framework.Graphics.Performance;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
@ -19,12 +21,28 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly HitObject HitObject;
|
public readonly HitObject HitObject;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of <see cref="HitObjectLifetimeEntry"/> for the <see cref="HitObject"/>'s nested objects (if any).
|
||||||
|
/// </summary>
|
||||||
|
public List<HitObjectLifetimeEntry> NestedEntries { get; internal set; } = new List<HitObjectLifetimeEntry>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The result that <see cref="HitObject"/> was judged with.
|
/// The result that <see cref="HitObject"/> was judged with.
|
||||||
/// This is set by the accompanying <see cref="DrawableHitObject"/>, and reused when required for rewinding.
|
/// This is set by the accompanying <see cref="DrawableHitObject"/>, and reused when required for rewinding.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal JudgementResult? Result;
|
internal JudgementResult? Result;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether <see cref="HitObject"/> has been judged.
|
||||||
|
/// Note: This does NOT include nested hitobjects.
|
||||||
|
/// </summary>
|
||||||
|
public bool Judged => Result?.HasResult ?? false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether <see cref="HitObject"/> and all of its nested objects have been judged.
|
||||||
|
/// </summary>
|
||||||
|
public bool AllJudged => Judged && NestedEntries.All(h => h.AllJudged);
|
||||||
|
|
||||||
private readonly IBindable<double> startTimeBindable = new BindableDouble();
|
private readonly IBindable<double> startTimeBindable = new BindableDouble();
|
||||||
|
|
||||||
internal event Action? RevertResult;
|
internal event Action? RevertResult;
|
||||||
|
@ -43,11 +43,6 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
private readonly Dictionary<HitObjectLifetimeEntry, HitObject> parentMap = new Dictionary<HitObjectLifetimeEntry, HitObject>();
|
private readonly Dictionary<HitObjectLifetimeEntry, HitObject> parentMap = new Dictionary<HitObjectLifetimeEntry, HitObject>();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stores the list of child entries for each hit object managed by this <see cref="HitObjectEntryManager"/>.
|
|
||||||
/// </summary>
|
|
||||||
private readonly Dictionary<HitObject, List<HitObjectLifetimeEntry>> childrenMap = new Dictionary<HitObject, List<HitObjectLifetimeEntry>>();
|
|
||||||
|
|
||||||
public void Add(HitObjectLifetimeEntry entry, HitObject? parent)
|
public void Add(HitObjectLifetimeEntry entry, HitObject? parent)
|
||||||
{
|
{
|
||||||
HitObject hitObject = entry.HitObject;
|
HitObject hitObject = entry.HitObject;
|
||||||
@ -57,22 +52,24 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
|||||||
|
|
||||||
// Add the entry.
|
// Add the entry.
|
||||||
entryMap[hitObject] = entry;
|
entryMap[hitObject] = entry;
|
||||||
childrenMap[hitObject] = new List<HitObjectLifetimeEntry>();
|
|
||||||
|
|
||||||
// If the entry has a parent, set it and add the entry to the parent's children.
|
// If the entry has a parent, set it and add the entry to the parent's children.
|
||||||
if (parent != null)
|
if (parent != null)
|
||||||
{
|
{
|
||||||
parentMap[entry] = parent;
|
parentMap[entry] = parent;
|
||||||
if (childrenMap.TryGetValue(parent, out var parentChildEntries))
|
if (entryMap.TryGetValue(parent, out var parentEntry))
|
||||||
parentChildEntries.Add(entry);
|
parentEntry.NestedEntries.Add(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
hitObject.DefaultsApplied += onDefaultsApplied;
|
hitObject.DefaultsApplied += onDefaultsApplied;
|
||||||
OnEntryAdded?.Invoke(entry, parent);
|
OnEntryAdded?.Invoke(entry, parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Remove(HitObjectLifetimeEntry entry)
|
public bool Remove(HitObjectLifetimeEntry entry)
|
||||||
{
|
{
|
||||||
|
if (entry is SyntheticHitObjectEntry)
|
||||||
|
return false;
|
||||||
|
|
||||||
HitObject hitObject = entry.HitObject;
|
HitObject hitObject = entry.HitObject;
|
||||||
|
|
||||||
if (!entryMap.ContainsKey(hitObject))
|
if (!entryMap.ContainsKey(hitObject))
|
||||||
@ -81,18 +78,16 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
|||||||
entryMap.Remove(hitObject);
|
entryMap.Remove(hitObject);
|
||||||
|
|
||||||
// If the entry has a parent, unset it and remove the entry from the parents' children.
|
// If the entry has a parent, unset it and remove the entry from the parents' children.
|
||||||
if (parentMap.Remove(entry, out var parent) && childrenMap.TryGetValue(parent, out var parentChildEntries))
|
if (parentMap.Remove(entry, out var parent) && entryMap.TryGetValue(parent, out var parentEntry))
|
||||||
parentChildEntries.Remove(entry);
|
parentEntry.NestedEntries.Remove(entry);
|
||||||
|
|
||||||
// Remove all the entries' children.
|
// Remove all the entries' children.
|
||||||
if (childrenMap.Remove(hitObject, out var childEntries))
|
foreach (var childEntry in entry.NestedEntries)
|
||||||
{
|
Remove(childEntry);
|
||||||
foreach (var childEntry in childEntries)
|
|
||||||
Remove(childEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
hitObject.DefaultsApplied -= onDefaultsApplied;
|
hitObject.DefaultsApplied -= onDefaultsApplied;
|
||||||
OnEntryRemoved?.Invoke(entry, parent);
|
OnEntryRemoved?.Invoke(entry, parent);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGet(HitObject hitObject, [MaybeNullWhen(false)] out HitObjectLifetimeEntry entry)
|
public bool TryGet(HitObject hitObject, [MaybeNullWhen(false)] out HitObjectLifetimeEntry entry)
|
||||||
@ -105,16 +100,16 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void onDefaultsApplied(HitObject hitObject)
|
private void onDefaultsApplied(HitObject hitObject)
|
||||||
{
|
{
|
||||||
if (!childrenMap.Remove(hitObject, out var childEntries))
|
if (!entryMap.TryGetValue(hitObject, out var entry))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Remove all the entries' children. At this point the parents' (this entries') children list has been removed from the map, so this does not cause upwards traversal.
|
// Replace the entire list rather than clearing to prevent circular traversal later.
|
||||||
foreach (var entry in childEntries)
|
var previousEntries = entry.NestedEntries;
|
||||||
Remove(entry);
|
entry.NestedEntries = new List<HitObjectLifetimeEntry>();
|
||||||
|
|
||||||
// The removed children list needs to be added back to the map for the entry to potentially receive children.
|
// Remove all the entries' children. At this point the parents' (this entries') children list has been reconstructed, so this does not cause upwards traversal.
|
||||||
childEntries.Clear();
|
foreach (var nested in previousEntries)
|
||||||
childrenMap[hitObject] = childEntries;
|
Remove(nested);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
40
osu.Game/Rulesets/Scoring/ILegacyScoreSimulator.cs
Normal file
40
osu.Game/Rulesets/Scoring/ILegacyScoreSimulator.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// 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.Mods;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Scoring
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Generates attributes which are required to calculate old-style Score V1 scores.
|
||||||
|
/// </summary>
|
||||||
|
public interface ILegacyScoreSimulator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The accuracy portion of the legacy (ScoreV1) total score.
|
||||||
|
/// </summary>
|
||||||
|
int AccuracyScore { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The combo-multiplied portion of the legacy (ScoreV1) total score.
|
||||||
|
/// </summary>
|
||||||
|
int ComboScore { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A ratio of <c>new_bonus_score / old_bonus_score</c> for converting the bonus score of legacy scores to the new scoring.
|
||||||
|
/// This is made up of all judgements that would be <see cref="HitResult.SmallBonus"/> or <see cref="HitResult.LargeBonus"/>.
|
||||||
|
/// </summary>
|
||||||
|
double BonusScoreRatio { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs the simulation, computing the maximum <see cref="AccuracyScore"/>, <see cref="ComboScore"/>,
|
||||||
|
/// and <see cref="BonusScoreRatio"/> achievable for the given beatmap.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="workingBeatmap">The working beatmap.</param>
|
||||||
|
/// <param name="playableBeatmap">A playable version of the beatmap for the ruleset.</param>
|
||||||
|
/// <param name="mods">The applied mods.</param>
|
||||||
|
void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods);
|
||||||
|
}
|
||||||
|
}
|
@ -28,7 +28,7 @@ namespace osu.Game.Scoring
|
|||||||
|
|
||||||
double? PP { get; }
|
double? PP { get; }
|
||||||
|
|
||||||
IBeatmapInfo Beatmap { get; }
|
IBeatmapInfo? Beatmap { get; }
|
||||||
|
|
||||||
IRulesetInfo Ruleset { get; }
|
IRulesetInfo Ruleset { get; }
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user