1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-19 08:32:55 +08:00

updated to latest version with slider calc

This commit is contained in:
Xexxar 2021-10-13 16:17:49 +00:00
commit 0b794728ea
445 changed files with 8643 additions and 3283 deletions

1
.github/FUNDING.yml vendored
View File

@ -1 +1,2 @@
github: ppy
custom: https://osu.ppy.sh/home/support custom: https://osu.ppy.sh/home/support

View File

@ -79,9 +79,14 @@ jobs:
run: | run: |
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround. # TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
# FIXME: Suppress warnings from templates project # FIXME: Suppress warnings from templates project
dotnet codefilesanity | while read -r line; do exit_code=0
echo "::warning::$line" while read -r line; do
done if [[ ! -z "$line" ]]; then
echo "::error::$line"
exit_code=1
fi
done <<< $(dotnet codefilesanity)
exit $exit_code
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded. # Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
# - name: .NET Format (Dry Run) # - name: .NET Format (Dry Run)

View File

@ -53,6 +53,7 @@ jobs:
diffcalc: diffcalc:
name: Run name: Run
runs-on: self-hosted runs-on: self-hosted
timeout-minutes: 1440
if: needs.metadata.outputs.continue == 'yes' if: needs.metadata.outputs.continue == 'yes'
needs: metadata needs: metadata
strategy: strategy:

View File

@ -51,11 +51,11 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.918.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.924.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.1013.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
<PackageReference Include="Realm" Version="10.5.0" /> <PackageReference Include="Realm" Version="10.6.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -140,10 +140,10 @@ namespace osu.Desktop
switch (activity) switch (activity)
{ {
case UserActivity.InGame game: case UserActivity.InGame game:
return game.Beatmap.ToString(); return game.BeatmapInfo.ToString();
case UserActivity.Editing edit: case UserActivity.Editing edit:
return edit.Beatmap.ToString(); return edit.BeatmapInfo.ToString();
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;

View File

@ -74,7 +74,10 @@ namespace osu.Desktop
// we want to allow multiple instances to be started when in debug. // we want to allow multiple instances to be started when in debug.
if (!DebugUtils.IsDebugBuild) if (!DebugUtils.IsDebugBuild)
{
Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error);
return 0; return 0;
}
} }
if (tournamentClient) if (tournamentClient)

View File

@ -14,11 +14,11 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
[TestCase(4.050601681491468d, "diffcalc-test")] [TestCase(4.0505463516206195d, "diffcalc-test")]
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, name); => base.Test(expected, name);
[TestCase(5.169743871843191d, "diffcalc-test")] [TestCase(5.1696411260785498d, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new CatchModDoubleTime()); => Test(expected, name, new CatchModDoubleTime());

View File

@ -29,8 +29,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
protected CatchSelectionBlueprintTestScene() protected CatchSelectionBlueprintTestScene()
{ {
EditorBeatmap = new EditorBeatmap(new CatchBeatmap()); EditorBeatmap = new EditorBeatmap(new CatchBeatmap()) { Difficulty = { CircleSize = 0 } };
EditorBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = 0;
EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint
{ {
BeatLength = 100 BeatLength = 100

View File

@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Edit.Blueprints; using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
protected override void AddHitObject(DrawableHitObject hitObject) protected override void AddHitObject(DrawableHitObject hitObject)
{ {
// Create nested bananas (but positions are not randomized because beatmap processing is not done). // Create nested bananas (but positions are not randomized because beatmap processing is not done).
hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), Beatmap.Value.BeatmapInfo.BaseDifficulty); hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
base.AddHitObject(hitObject); base.AddHitObject(hitObject);
} }

View File

@ -4,9 +4,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Edit.Blueprints; using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
@ -23,11 +23,12 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream; private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
[BackgroundDependencyLoader] protected override IBeatmap GetPlayableBeatmap()
private void load()
{ {
Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderTickRate = 5; var playable = base.GetPlayableBeatmap();
Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity * 10; playable.Difficulty.SliderTickRate = 5;
playable.Difficulty.SliderMultiplier = velocity * 10;
return playable;
} }
[Test] [Test]

View File

@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
X = x, X = x,
Path = sliderPath, Path = sliderPath,
}; };
EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity; EditorBeatmap.Difficulty.SliderMultiplier = velocity;
EditorBeatmap.Add(hitObject); EditorBeatmap.Add(hitObject);
EditorBeatmap.Update(hitObject); EditorBeatmap.Update(hitObject);
Assert.That(hitObject.Velocity, Is.EqualTo(velocity)); Assert.That(hitObject.Velocity, Is.EqualTo(velocity));

View File

@ -290,7 +290,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
public IEnumerable<CaughtObject> CaughtObjects => this.ChildrenOfType<CaughtObject>(); public IEnumerable<CaughtObject> CaughtObjects => this.ChildrenOfType<CaughtObject>();
public TestCatcher(DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty) public TestCatcher(DroppedObjectContainer droppedObjectTarget, IBeatmapDifficultyInfo difficulty)
: base(droppedObjectTarget, difficulty) : base(droppedObjectTarget, difficulty)
{ {
} }
@ -298,7 +298,7 @@ namespace osu.Game.Rulesets.Catch.Tests
public class TestKiaiFruit : Fruit public class TestKiaiFruit : Fruit
{ {
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{ {
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true }); controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private ScheduledDelegate addManyFruit; private ScheduledDelegate addManyFruit;
private BeatmapDifficulty beatmapDifficulty; private IBeatmapDifficultyInfo beatmapDifficulty;
public TestSceneCatcherArea() public TestSceneCatcherArea()
{ {
@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private class TestCatcherArea : CatcherArea private class TestCatcherArea : CatcherArea
{ {
public TestCatcherArea(BeatmapDifficulty beatmapDifficulty) public TestCatcherArea(IBeatmapDifficultyInfo beatmapDifficulty)
{ {
var droppedObjectContainer = new DroppedObjectContainer(); var droppedObjectContainer = new DroppedObjectContainer();
Add(droppedObjectContainer); Add(droppedObjectContainer);

View File

@ -211,7 +211,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2; double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) / 2;
// Todo: This is wrong. osu!stable calculated hyperdashes using the full catcher size, excluding the margins. // Todo: This is wrong. osu!stable calculated hyperdashes using the full catcher size, excluding the margins.
// This should theoretically cause impossible scenarios, but practically, likely due to the size of the playfield, it doesn't seem possible. // This should theoretically cause impossible scenarios, but practically, likely due to the size of the playfield, it doesn't seem possible.

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
return new CatchDifficultyAttributes { Mods = mods, Skills = skills }; return new CatchDifficultyAttributes { Mods = mods, Skills = skills };
// 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 = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
return new CatchDifficultyAttributes return new CatchDifficultyAttributes
{ {
@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
// In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream. // In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream.
foreach (var hitObject in beatmap.HitObjects foreach (var hitObject in beatmap.HitObjects
.SelectMany(obj => obj is JuiceStream stream ? stream.NestedHitObjects : new[] { obj }) .SelectMany(obj => obj is JuiceStream stream ? stream.NestedHitObjects.AsEnumerable() : new[] { obj })
.Cast<CatchHitObject>() .Cast<CatchHitObject>()
.OrderBy(x => x.StartTime)) .OrderBy(x => x.StartTime))
{ {
@ -69,10 +69,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
{ {
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f; halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) * 0.5f;
// For circle sizes above 5.5, reduce the catcher width further to simulate imperfect gameplay. // For circle sizes above 5.5, reduce the catcher width further to simulate imperfect gameplay.
halfCatcherWidth *= 1 - (Math.Max(0, beatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5.5f) * 0.0625f); halfCatcherWidth *= 1 - (Math.Max(0, beatmap.Difficulty.CircleSize - 5.5f) * 0.0625f);
return new Skill[] return new Skill[]
{ {

View File

@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Catch.Edit
public class CatchEditorPlayfield : CatchPlayfield public class CatchEditorPlayfield : CatchPlayfield
{ {
// TODO fixme: the size of the catcher is not changed when circle size is changed in setup screen. // TODO fixme: the size of the catcher is not changed when circle size is changed in setup screen.
public CatchEditorPlayfield(BeatmapDifficulty difficulty) public CatchEditorPlayfield(IBeatmapDifficultyInfo difficulty)
: base(difficulty) : base(difficulty)
{ {
} }

View File

@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Catch.Edit
{ {
} }
protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.BeatmapInfo.BaseDifficulty); protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.Difficulty);
} }
} }

View File

@ -128,11 +128,11 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary> /// </summary>
public int RandomSeed => (int)StartTime; public int RandomSeed => (int)StartTime;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2; Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2;
} }

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary> /// </summary>
public double SpanDuration => Duration / this.SpanCount(); public double SpanDuration => Duration / this.SpanCount();
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);

View File

@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Catch.UI
internal CatcherArea CatcherArea { get; private set; } internal CatcherArea CatcherArea { get; private set; }
private readonly BeatmapDifficulty difficulty; private readonly IBeatmapDifficultyInfo difficulty;
public CatchPlayfield(BeatmapDifficulty difficulty) public CatchPlayfield(IBeatmapDifficultyInfo difficulty)
{ {
this.difficulty = difficulty; this.difficulty = difficulty;
} }

View File

@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Catch.UI
private readonly DrawablePool<CaughtBanana> caughtBananaPool; private readonly DrawablePool<CaughtBanana> caughtBananaPool;
private readonly DrawablePool<CaughtDroplet> caughtDropletPool; private readonly DrawablePool<CaughtDroplet> caughtDropletPool;
public Catcher([NotNull] DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty = null) public Catcher([NotNull] DroppedObjectContainer droppedObjectTarget, IBeatmapDifficultyInfo difficulty = null)
{ {
this.droppedObjectTarget = droppedObjectTarget; this.droppedObjectTarget = droppedObjectTarget;
@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// <summary> /// <summary>
/// Calculates the scale of the catcher based off the provided beatmap difficulty. /// Calculates the scale of the catcher based off the provided beatmap difficulty.
/// </summary> /// </summary>
private static Vector2 calculateScale(BeatmapDifficulty difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); private static Vector2 calculateScale(IBeatmapDifficultyInfo difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
/// <summary> /// <summary>
/// Calculates the width of the area used for attempting catches in gameplay. /// Calculates the width of the area used for attempting catches in gameplay.
@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// Calculates the width of the area used for attempting catches in gameplay. /// Calculates the width of the area used for attempting catches in gameplay.
/// </summary> /// </summary>
/// <param name="difficulty">The beatmap difficulty.</param> /// <param name="difficulty">The beatmap difficulty.</param>
public static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty)); public static float CalculateCatchWidth(IBeatmapDifficultyInfo difficulty) => CalculateCatchWidth(calculateScale(difficulty));
/// <summary> /// <summary>
/// Determine if this catcher can catch a <see cref="CatchHitObject"/> in the current position. /// Determine if this catcher can catch a <see cref="CatchHitObject"/> in the current position.

View File

@ -27,14 +27,14 @@ namespace osu.Game.Rulesets.Catch.UI
: base(ruleset, beatmap, mods) : base(ruleset, beatmap, mods)
{ {
Direction.Value = ScrollingDirection.Down; Direction.Value = ScrollingDirection.Down;
TimeRange.Value = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450); TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450);
} }
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield); protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield);
protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty); protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.Difficulty);
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchPlayfieldAdjustmentContainer(); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchPlayfieldAdjustmentContainer();

View File

@ -42,8 +42,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
{ {
IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo); IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
var roundedCircleSize = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.CircleSize); var roundedCircleSize = Math.Round(beatmap.Difficulty.CircleSize);
var roundedOverallDifficulty = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); var roundedOverallDifficulty = Math.Round(beatmap.Difficulty.OverallDifficulty);
if (IsForCurrentRuleset) if (IsForCurrentRuleset)
{ {
@ -71,9 +71,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
originalTargetColumns = TargetColumns; originalTargetColumns = TargetColumns;
} }
public static int GetColumnCountForNonConvert(BeatmapInfo beatmap) public static int GetColumnCountForNonConvert(BeatmapInfo beatmapInfo)
{ {
var roundedCircleSize = Math.Round(beatmap.BaseDifficulty.CircleSize); var roundedCircleSize = Math.Round(beatmapInfo.BaseDifficulty.CircleSize);
return (int)Math.Max(1, roundedCircleSize); return (int)Math.Max(1, roundedCircleSize);
} }
@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{ {
BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty; IBeatmapDifficultyInfo difficulty = original.Difficulty;
int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate); int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
Random = new FastRandom(seed); Random = new FastRandom(seed);

View File

@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
StartTime = (int)Math.Round(hitObject.StartTime); StartTime = (int)Math.Round(hitObject.StartTime);
// This matches stable's calculation. // This matches stable's calculation.
EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier); EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.Difficulty.SliderMultiplier);
SegmentDuration = (EndTime - StartTime) / SpanCount; SegmentDuration = (EndTime - StartTime) / SpanCount;
} }

View File

@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (drainTime == 0) if (drainTime == 0)
drainTime = 10000; drainTime = 10000;
BeatmapDifficulty difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty; IBeatmapDifficultyInfo difficulty = OriginalBeatmap.Difficulty;
conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15; conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
conversionDifficulty = Math.Min(conversionDifficulty.Value, 12); conversionDifficulty = Math.Min(conversionDifficulty.Value, 12);
@ -149,7 +149,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{ {
lowerBound ??= RandomStart; lowerBound ??= RandomStart;
upperBound ??= TotalColumns; upperBound ??= TotalColumns;
nextColumn ??= (_ => GetRandomColumn(lowerBound, upperBound)); nextColumn ??= _ => GetRandomColumn(lowerBound, upperBound);
// Check for the initial column // Check for the initial column
if (isValid(initialColumn)) if (isValid(initialColumn))
@ -176,7 +176,19 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return initialColumn; return initialColumn;
bool isValid(int column) => validation?.Invoke(column) != false && !patterns.Any(p => p.ColumnHasObject(column)); bool isValid(int column)
{
if (validation?.Invoke(column) == false)
return false;
foreach (var p in patterns)
{
if (p.ColumnHasObject(column))
return false;
}
return true;
}
} }
/// <summary> /// <summary>

View File

@ -12,46 +12,68 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
/// </summary> /// </summary>
internal class Pattern internal class Pattern
{ {
private readonly List<ManiaHitObject> hitObjects = new List<ManiaHitObject>(); private List<ManiaHitObject> hitObjects;
private HashSet<int> containedColumns;
/// <summary> /// <summary>
/// All the hit objects contained in this pattern. /// All the hit objects contained in this pattern.
/// </summary> /// </summary>
public IEnumerable<ManiaHitObject> HitObjects => hitObjects; public IEnumerable<ManiaHitObject> HitObjects => hitObjects ?? Enumerable.Empty<ManiaHitObject>();
/// <summary> /// <summary>
/// Check whether a column of this patterns contains a hit object. /// Check whether a column of this patterns contains a hit object.
/// </summary> /// </summary>
/// <param name="column">The column index.</param> /// <param name="column">The column index.</param>
/// <returns>Whether the column with index <paramref name="column"/> contains a hit object.</returns> /// <returns>Whether the column with index <paramref name="column"/> contains a hit object.</returns>
public bool ColumnHasObject(int column) => hitObjects.Exists(h => h.Column == column); public bool ColumnHasObject(int column) => containedColumns?.Contains(column) == true;
/// <summary> /// <summary>
/// Amount of columns taken up by hit objects in this pattern. /// Amount of columns taken up by hit objects in this pattern.
/// </summary> /// </summary>
public int ColumnWithObjects => HitObjects.GroupBy(h => h.Column).Count(); public int ColumnWithObjects => containedColumns?.Count ?? 0;
/// <summary> /// <summary>
/// Adds a hit object to this pattern. /// Adds a hit object to this pattern.
/// </summary> /// </summary>
/// <param name="hitObject">The hit object to add.</param> /// <param name="hitObject">The hit object to add.</param>
public void Add(ManiaHitObject hitObject) => hitObjects.Add(hitObject); public void Add(ManiaHitObject hitObject)
{
prepareStorage();
hitObjects.Add(hitObject);
containedColumns.Add(hitObject.Column);
}
/// <summary> /// <summary>
/// Copies hit object from another pattern to this one. /// Copies hit object from another pattern to this one.
/// </summary> /// </summary>
/// <param name="other">The other pattern.</param> /// <param name="other">The other pattern.</param>
public void Add(Pattern other) => hitObjects.AddRange(other.HitObjects); public void Add(Pattern other)
{
prepareStorage();
if (other.hitObjects != null)
{
hitObjects.AddRange(other.hitObjects);
foreach (var h in other.hitObjects)
containedColumns.Add(h.Column);
}
}
/// <summary> /// <summary>
/// Clears this pattern, removing all hit objects. /// Clears this pattern, removing all hit objects.
/// </summary> /// </summary>
public void Clear() => hitObjects.Clear(); public void Clear()
{
hitObjects?.Clear();
containedColumns?.Clear();
}
/// <summary> private void prepareStorage()
/// Removes a hit object from this pattern. {
/// </summary> hitObjects ??= new List<ManiaHitObject>();
/// <param name="hitObject">The hit object to remove.</param> containedColumns ??= new HashSet<int>();
public bool Remove(ManiaHitObject hitObject) => hitObjects.Remove(hitObject); }
} }
} }

View File

@ -41,15 +41,14 @@ namespace osu.Game.Rulesets.Mania.Difficulty
return new ManiaDifficultyAttributes { Mods = mods, Skills = skills }; return new ManiaDifficultyAttributes { Mods = mods, Skills = skills };
HitWindows hitWindows = new ManiaHitWindows(); HitWindows hitWindows = new ManiaHitWindows();
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
return new ManiaDifficultyAttributes return new ManiaDifficultyAttributes
{ {
StarRating = skills[0].DifficultyValue() * star_scaling_factor, StarRating = skills[0].DifficultyValue() * star_scaling_factor,
Mods = mods, Mods = mods,
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future GreatHitWindow = Math.Ceiling(getHitWindow300(mods) / clockRate),
GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate), ScoreMultiplier = getScoreMultiplier(mods),
ScoreMultiplier = getScoreMultiplier(beatmap, mods),
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1), MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
Skills = skills Skills = skills
}; };
@ -70,7 +69,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[] protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
{ {
new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns) new Strain(mods, ((ManiaBeatmap)Beatmap).TotalColumns)
}; };
protected override Mod[] DifficultyAdjustmentMods protected override Mod[] DifficultyAdjustmentMods
@ -138,7 +137,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
} }
} }
private double getScoreMultiplier(IBeatmap beatmap, Mod[] mods) private double getScoreMultiplier(Mod[] mods)
{ {
double scoreMultiplier = 1; double scoreMultiplier = 1;
@ -154,7 +153,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
} }
} }
var maniaBeatmap = (ManiaBeatmap)beatmap; var maniaBeatmap = (ManiaBeatmap)Beatmap;
int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns; int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns;
if (diff > 0) if (diff > 0)

View File

@ -13,9 +13,9 @@ namespace osu.Game.Rulesets.Mania
{ {
private FilterCriteria.OptionalRange<float> keys; private FilterCriteria.OptionalRange<float> keys;
public bool Matches(BeatmapInfo beatmap) public bool Matches(BeatmapInfo beatmapInfo)
{ {
return !keys.HasFilter || (beatmap.RulesetID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmap))); return !keys.HasFilter || (beatmapInfo.RulesetID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo)));
} }
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)

View File

@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Objects
/// </summary> /// </summary>
private double tickSpacing = 50; private double tickSpacing = 50;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);

View File

@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Mania.UI
{ {
// Mania doesn't care about global velocity // Mania doesn't care about global velocity
p.Velocity = 1; p.Velocity = 1;
p.BaseBeatLength *= Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier; p.BaseBeatLength *= Beatmap.Difficulty.SliderMultiplier;
// For non-mania beatmap, speed changes should only happen through timing points // For non-mania beatmap, speed changes should only happen through timing points
if (!isForCurrentRuleset) if (!isForCurrentRuleset)

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
public class CheckTooShortSpinnersTest public class CheckTooShortSpinnersTest
{ {
private CheckTooShortSpinners check; private CheckTooShortSpinners check;
private BeatmapDifficulty difficulty; private IBeatmapDifficultyInfo difficulty;
[SetUp] [SetUp]
public void Setup() public void Setup()
@ -81,12 +81,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
assertTooShort(new List<HitObject> { spinnerHighOd }, difficultyHighOd); assertTooShort(new List<HitObject> { spinnerHighOd }, difficultyHighOd);
} }
private void assertOk(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty) private void assertOk(List<HitObject> hitObjects, IBeatmapDifficultyInfo beatmapDifficulty)
{ {
Assert.That(check.Run(getContext(hitObjects, beatmapDifficulty)), Is.Empty); Assert.That(check.Run(getContext(hitObjects, beatmapDifficulty)), Is.Empty);
} }
private void assertVeryShort(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty) private void assertVeryShort(List<HitObject> hitObjects, IBeatmapDifficultyInfo beatmapDifficulty)
{ {
var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList(); var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList();
@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateVeryShort); Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateVeryShort);
} }
private void assertTooShort(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty) private void assertTooShort(List<HitObject> hitObjects, IBeatmapDifficultyInfo beatmapDifficulty)
{ {
var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList(); var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList();
@ -102,12 +102,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateTooShort); Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateTooShort);
} }
private BeatmapVerifierContext getContext(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty) private BeatmapVerifierContext getContext(List<HitObject> hitObjects, IBeatmapDifficultyInfo beatmapDifficulty)
{ {
var beatmap = new Beatmap<HitObject> var beatmap = new Beatmap<HitObject>
{ {
HitObjects = hitObjects, HitObjects = hitObjects,
BeatmapInfo = new BeatmapInfo { BaseDifficulty = beatmapDifficulty } BeatmapInfo = new BeatmapInfo { BaseDifficulty = new BeatmapDifficulty(beatmapDifficulty) }
}; };
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[SetUp] [SetUp]
public void Setup() => Schedule(() => public void Setup() => Schedule(() =>
{ {
editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1; editorBeatmap.Difficulty.SliderMultiplier = 1;
editorBeatmap.ControlPointInfo.Clear(); editorBeatmap.ControlPointInfo.Clear();
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length }); editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });

View File

@ -45,8 +45,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{ {
new Spinner new Spinner
{ {
Duration = 2000, Duration = 6000,
Position = OsuPlayfield.BASE_SIZE / 2 Position = OsuPlayfield.BASE_SIZE / 2,
} }
} }
}, },

View File

@ -4,13 +4,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Play;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods namespace osu.Game.Rulesets.Osu.Tests.Mods
@ -122,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
private bool checkSomeHit() => Player.ScoreProcessor.JudgedHits >= 4; private bool checkSomeHit() => Player.ScoreProcessor.JudgedHits >= 4;
private bool objectWithIncreasedVisibilityHasIndex(int index) private bool objectWithIncreasedVisibilityHasIndex(int index)
=> Player.Mods.Value.OfType<TestOsuModHidden>().Single().FirstObject == Player.ChildrenOfType<GameplayBeatmap>().Single().HitObjects[index]; => Player.GameplayState.Mods.OfType<TestOsuModHidden>().Single().FirstObject == Player.GameplayState.Beatmap.HitObjects[index];
private class TestOsuModHidden : OsuModHidden private class TestOsuModHidden : OsuModHidden
{ {

View File

@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.6634445062299665d, "diffcalc-test")] [TestCase(6.5867229481955389d, "diffcalc-test")]
[TestCase(1.0414203870195022d, "zero-length-sliders")] [TestCase(1.0416315570967911d, "zero-length-sliders")]
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, name); => base.Test(expected, name);
[TestCase(8.3858089051603368d, "diffcalc-test")] [TestCase(8.2730989071947896d, "diffcalc-test")]
[TestCase(1.2723279173428435d, "zero-length-sliders")] [TestCase(1.2726413186221039d, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new OsuModDoubleTime()); => Test(expected, name, new OsuModDoubleTime());

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,3 @@
[General]
Version: latest
HitCircleOverlayAboveNumber: 0

View File

@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
Position = new Vector2(100, 300), Position = new Vector2(100, 300),
}, },
accuracyHeatmap = new TestAccuracyHeatmap(new ScoreInfo { Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }) accuracyHeatmap = new TestAccuracyHeatmap(new ScoreInfo { BeatmapInfo = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo })
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -17,6 +17,7 @@ using osu.Framework.Testing.Input;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public class TestSceneGameplayCursor : OsuSkinnableTestScene public class TestSceneGameplayCursor : OsuSkinnableTestScene
{ {
[Cached] [Cached]
private GameplayBeatmap gameplayBeatmap; private GameplayState gameplayState;
private OsuCursorContainer lastContainer; private OsuCursorContainer lastContainer;
@ -40,7 +41,8 @@ namespace osu.Game.Rulesets.Osu.Tests
public TestSceneGameplayCursor() public TestSceneGameplayCursor()
{ {
gameplayBeatmap = new GameplayBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)); var ruleset = new OsuRuleset();
gameplayState = new GameplayState(CreateBeatmap(ruleset.RulesetInfo), ruleset, Array.Empty<Mod>());
AddStep("change background colour", () => AddStep("change background colour", () =>
{ {
@ -57,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddSliderStep("circle size", 0f, 10f, 0f, val => AddSliderStep("circle size", 0f, 10f, 0f, val =>
{ {
config.SetValue(OsuSetting.AutoCursorSize, true); config.SetValue(OsuSetting.AutoCursorSize, true);
gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = val; gameplayState.Beatmap.Difficulty.CircleSize = val;
Scheduler.AddOnce(() => loadContent(false)); Scheduler.AddOnce(() => loadContent(false));
}); });
@ -73,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public void TestSizing(int circleSize, float userScale) public void TestSizing(int circleSize, float userScale)
{ {
AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale)); AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale));
AddStep($"adjust cs to {circleSize}", () => gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize); AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.Difficulty.CircleSize = circleSize);
AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true)); AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true));
AddStep("load content", () => loadContent()); AddStep("load content", () => loadContent());

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests
}; };
var hitWindows = new OsuHitWindows(); var hitWindows = new OsuHitWindows();
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
CreateModTest(new ModTestData CreateModTest(new ModTestData
{ {
@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests
}; };
var hitWindows = new OsuHitWindows(); var hitWindows = new OsuHitWindows();
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
CreateModTest(new ModTestData CreateModTest(new ModTestData
{ {

View File

@ -400,9 +400,9 @@ namespace osu.Game.Rulesets.Osu.Tests
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject> Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
{ {
HitObjects = hitObjects, HitObjects = hitObjects,
Difficulty = new BeatmapDifficulty { SliderTickRate = 3 },
BeatmapInfo = BeatmapInfo =
{ {
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
Ruleset = new OsuRuleset().RulesetInfo Ruleset = new OsuRuleset().RulesetInfo
}, },
}); });
@ -452,7 +452,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private class TestSpinner : Spinner private class TestSpinner : Spinner
{ {
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
SpinsRequired = 1; SpinsRequired = 1;

View File

@ -30,6 +30,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public class TestSceneSpinnerRotation : TestSceneOsuPlayer public class TestSceneSpinnerRotation : TestSceneOsuPlayer
{ {
private const double spinner_start_time = 100;
private const double spinner_duration = 6000;
[Resolved] [Resolved]
private AudioManager audioManager { get; set; } private AudioManager audioManager { get; set; }
@ -77,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests
double finalTrackerRotation = 0, trackerRotationTolerance = 0; double finalTrackerRotation = 0, trackerRotationTolerance = 0;
double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0; double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
addSeekStep(5000); addSeekStep(spinner_start_time + 5000);
AddStep("retrieve disc rotation", () => AddStep("retrieve disc rotation", () =>
{ {
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation; finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests
}); });
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation); AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation);
addSeekStep(2500); addSeekStep(spinner_start_time + 2500);
AddAssert("disc rotation rewound", AddAssert("disc rotation rewound",
// we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in. // we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
// due to the exponential damping applied we're allowing a larger margin of error of about 10% // due to the exponential damping applied we're allowing a larger margin of error of about 10%
@ -102,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Tests
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error. // cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
() => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100)); () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100));
addSeekStep(5000); addSeekStep(spinner_start_time + 5000);
AddAssert("is disc rotation almost same", AddAssert("is disc rotation almost same",
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance)); () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance));
AddAssert("is symbol rotation almost same", AddAssert("is symbol rotation almost same",
@ -140,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test] [Test]
public void TestSpinnerNormalBonusRewinding() public void TestSpinnerNormalBonusRewinding()
{ {
addSeekStep(1000); addSeekStep(spinner_start_time + 1000);
AddAssert("player score matching expected bonus score", () => AddAssert("player score matching expected bonus score", () =>
{ {
@ -201,24 +204,9 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0)); AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
} }
private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
{
Frames = scoreReplay
.Frames
.Cast<OsuReplayFrame>()
.Select(replayFrame =>
{
var adjustedTime = replayFrame.Time * rate;
return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray());
})
.Cast<ReplayFrame>()
.ToList()
};
private void addSeekStep(double time) private void addSeekStep(double time)
{ {
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time)); AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
} }
@ -241,7 +229,8 @@ namespace osu.Game.Rulesets.Osu.Tests
new Spinner new Spinner
{ {
Position = new Vector2(256, 192), Position = new Vector2(256, 192),
EndTime = 6000, StartTime = spinner_start_time,
Duration = spinner_duration
}, },
} }
}; };

View File

@ -412,7 +412,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private class TestSpinner : Spinner private class TestSpinner : Spinner
{ {
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
SpinsRequired = 1; SpinsRequired = 1;

View File

@ -12,6 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
public double FlashlightRating { get; set; } public double FlashlightRating { get; set; }
public double ApproachRate { get; set; } public double ApproachRate { get; set; }
public double OverallDifficulty { get; set; } public double OverallDifficulty { get; set; }
public double DrainRate { get; set; }
public int HitCircleCount { get; set; } public int HitCircleCount { get; set; }
public int SpinnerCount { get; set; } public int SpinnerCount { get; set; }
} }

View File

@ -37,6 +37,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
double flashlightRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier; double flashlightRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
if (mods.Any(h => h is OsuModRelax))
speedRating = 0.0;
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000; double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000; double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
double baseFlashlightPerformance = 0.0; double baseFlashlightPerformance = 0.0;
@ -53,7 +56,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate;
int maxCombo = beatmap.HitObjects.Count; int maxCombo = beatmap.HitObjects.Count;
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above) // Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
@ -71,6 +75,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
FlashlightRating = flashlightRating, FlashlightRating = flashlightRating,
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6, OverallDifficulty = (80 - hitWindowGreat) / 6,
DrainRate = drainRate,
MaxCombo = maxCombo, MaxCombo = maxCombo,
HitCircleCount = hitCirclesCount, HitCircleCount = hitCirclesCount,
SpinnerCount = spinnerCount, SpinnerCount = spinnerCount,
@ -95,10 +100,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
{ {
HitWindows hitWindows = new OsuHitWindows(); HitWindows hitWindows = new OsuHitWindows();
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
// Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate;
return new Skill[] return new Skill[]
{ {

View File

@ -40,15 +40,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
// Custom multipliers for NoFail and SpunOut.
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
// Custom multipliers for NoFail and SpunOut.
if (mods.Any(m => m is OsuModNoFail)) if (mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss); multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss);
if (mods.Any(m => m is OsuModSpunOut)) if (mods.Any(m => m is OsuModSpunOut))
multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85); multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85);
if (mods.Any(h => h is OsuModRelax))
{
countMiss += countOk + countMeh;
multiplier *= 0.6;
}
double aimValue = computeAimValue(); double aimValue = computeAimValue();
double speedValue = computeSpeedValue(); double speedValue = computeSpeedValue();
double accuracyValue = computeAccuracyValue(); double accuracyValue = computeAccuracyValue();
@ -108,14 +114,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor; double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. if (mods.Any(m => m is OsuModBlinds))
if (mods.Any(h => h is OsuModHidden)) aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * countMiss)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate);
else if (mods.Any(h => h is OsuModHidden))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
}
aimValue *= approachRateBonus; aimValue *= approachRateBonus;
// Scale the aim value with accuracy _slightly_. // Scale the aim value with accuracy
aimValue *= 0.5 + accuracy / 2.0; aimValue *= accuracy;
// It is important to also consider accuracy difficulty when doing that. // It is important to also consider accuracy difficulty when doing that.
aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500; aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
@ -147,11 +157,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor; speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
if (mods.Any(m => m is OsuModHidden)) if (mods.Any(m => m is OsuModBlinds))
{
// Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.
speedValue *= 1.12;
}
else if (mods.Any(m => m is OsuModHidden))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
}
// Scale the speed value with accuracy and OD. // Scale the speed value with accuracy and OD.
speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2); speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping. // Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
@ -160,6 +179,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeAccuracyValue() private double computeAccuracyValue()
{ {
if (mods.Any(h => h is OsuModRelax))
return 0.0;
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window. // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
double betterAccuracyPercentage; double betterAccuracyPercentage;
int amountHitObjectsWithAccuracy = Attributes.HitCircleCount; int amountHitObjectsWithAccuracy = Attributes.HitCircleCount;
@ -180,8 +202,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer. // Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3)); accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
if (mods.Any(m => m is OsuModHidden)) // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
if (mods.Any(m => m is OsuModBlinds))
accuracyValue *= 1.14;
else if (mods.Any(m => m is OsuModHidden))
accuracyValue *= 1.08; accuracyValue *= 1.08;
if (mods.Any(m => m is OsuModFlashlight)) if (mods.Any(m => m is OsuModFlashlight))
accuracyValue *= 1.02; accuracyValue *= 1.02;

View File

@ -22,25 +22,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
public double JumpDistance { get; private set; } public double JumpDistance { get; private set; }
/// <summary> /// <summary>
/// Normalized Vector from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>. /// Minimum distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
/// </summary> /// </summary>
public Vector2 JumpVector { get; private set; } public double MovementDistance { get; private set; }
/// <summary> /// <summary>JumpTravel
/// Normalized distance between the start and end position of the previous <see cref="OsuDifficultyHitObject"/>. /// Normalized distance between the start and end position of the previous <see cref="OsuDifficultyHitObject"/>.
/// </summary> /// </summary>
public double TravelDistance { get; private set; } public double TravelDistance { get; private set; }
/// <summary>
/// Normalized Vector from the start position of the previous <see cref="OsuDifficultyHitObject"/> to the end position of the previous <see cref="OsuDifficultyHitObject"/>.
/// </summary>
public Vector2 TravelVector { get; private set; }
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 50ms.
/// </summary>
public readonly double TravelTime;
/// <summary> /// <summary>
/// Angle the player has to take to hit this <see cref="OsuDifficultyHitObject"/>. /// Angle the player has to take to hit this <see cref="OsuDifficultyHitObject"/>.
/// Calculated as the angle between the circles (current-2, current-1, current). /// Calculated as the angle between the circles (current-2, current-1, current).
@ -48,7 +38,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
public double? Angle { get; private set; } public double? Angle { get; private set; }
/// <summary> /// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 50ms. /// Milliseconds elapsed since the end time of the Previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public double MovementTime { get; private set; }
/// <summary>
/// Milliseconds elapsed since from the start time of the Previous <see cref="OsuDifficultyHitObject"/> to the end time of the same Previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public double TravelTime { get; private set; }
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary> /// </summary>
public readonly double StrainTime; public readonly double StrainTime;
@ -61,13 +61,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
this.lastLastObject = (OsuHitObject)lastLastObject; this.lastLastObject = (OsuHitObject)lastLastObject;
this.lastObject = (OsuHitObject)lastObject; this.lastObject = (OsuHitObject)lastObject;
setDistances();
// Capped to 25ms to prevent difficulty calculation breaking from simulatenous objects. // Capped to 25ms to prevent difficulty calculation breaking from simulatenous objects.
StrainTime = Math.Max(DeltaTime, 25); StrainTime = Math.Max(DeltaTime, 25);
setDistances(clockRate);
} }
private void setDistances() private void setDistances(double clockRate)
{ {
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps. // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
float scalingFactor = normalized_radius / (float)BaseObject.Radius; float scalingFactor = normalized_radius / (float)BaseObject.Radius;
@ -81,9 +81,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
if (lastObject is Slider lastSlider) if (lastObject is Slider lastSlider)
{ {
computeSliderCursorPosition(lastSlider); computeSliderCursorPosition(lastSlider);
TravelVector = Vector2.Multiply(Vector2.Subtract(lastSlider.TailCircle.Position, lastSlider.HeadCircle.Position), scalingFactor);
TravelDistance = lastSlider.LazyTravelDistance * scalingFactor; TravelDistance = lastSlider.LazyTravelDistance * scalingFactor;
TravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, 0);
MovementTime = Math.Max(StrainTime - TravelTime, 0);
MovementDistance = Math.Max(0, Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length - 0) * scalingFactor;
} }
Vector2 lastCursorPosition = getEndCursorPosition(lastObject); Vector2 lastCursorPosition = getEndCursorPosition(lastObject);
@ -91,8 +92,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
// Don't need to jump to reach spinners // Don't need to jump to reach spinners
if (!(BaseObject is Spinner)) if (!(BaseObject is Spinner))
{ {
JumpVector = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor); JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
JumpDistance = JumpVector.Length; MovementDistance = Math.Min(JumpDistance, MovementDistance);
} }
if (lastLastObject != null) if (lastLastObject != null)
@ -129,6 +130,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
var diff = slider.StackedPosition + slider.Path.PositionAt(progress) - slider.LazyEndPosition.Value; var diff = slider.StackedPosition + slider.Path.PositionAt(progress) - slider.LazyEndPosition.Value;
float dist = diff.Length; float dist = diff.Length;
slider.LazyTravelTime = t - slider.StartTime;
if (dist > approxFollowCircleRadius) if (dist > approxFollowCircleRadius)
{ {
// The cursor would be outside the follow circle, we need to move it // The cursor would be outside the follow circle, we need to move it

View File

@ -7,7 +7,6 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Framework.Utils; using osu.Framework.Utils;
using osuTK;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{ {
@ -23,15 +22,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
protected override int HistoryLength => 2; protected override int HistoryLength => 2;
protected override double SkillMultiplier => 24.75; private const double wide_angle_multiplier = 1.5;
protected override double StrainDecayBase => 0.15; private const double acute_angle_multiplier = 2.0;
private const double slider_multiplier = 2.75;
private const double wide_angle_multiplier = 1.0; private double currentStrain = 1;
private const double acute_angle_multiplier = 1.0;
private const double rhythm_variance_multiplier = 1.0;
private const double slider_multiplier = 4.25;
protected override double StrainValueOf(DifficultyHitObject current) private double skillMultiplier => 23.25;
private double strainDecayBase => 0.15;
private double strainValueOf(DifficultyHitObject current)
{ {
if (current.BaseObject is Spinner || Previous.Count <= 1) if (current.BaseObject is Spinner || Previous.Count <= 1)
return 0; return 0;
@ -40,82 +40,84 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
var osuPrevObj = (OsuDifficultyHitObject)Previous[0]; var osuPrevObj = (OsuDifficultyHitObject)Previous[0];
var osuLastObj = (OsuDifficultyHitObject)Previous[1]; var osuLastObj = (OsuDifficultyHitObject)Previous[1];
var currVector = Vector2.Divide(osuCurrObj.JumpVector, (float)osuCurrObj.StrainTime); double currVelocity = osuCurrObj.JumpDistance / osuCurrObj.StrainTime; // Start iwth the base distance / time
var prevVector = Vector2.Divide(osuPrevObj.JumpVector, (float)osuPrevObj.StrainTime);
// Start with regular velocity. if (osuPrevObj.BaseObject is Slider) // If object is a slider
double aimStrain = currVector.Length;
if (Precision.AlmostEquals(osuCurrObj.StrainTime, osuPrevObj.StrainTime, 10)) // Rhythms are the same.
{ {
if (osuCurrObj.Angle != null) double movementVelocity = osuCurrObj.MovementDistance / osuCurrObj.MovementTime; // calculate the movement velocity from slider end to next note
double travelVelocity = osuCurrObj.TravelDistance / osuCurrObj.TravelTime; // calculate the slider velocity from slider head to lazy end.
currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity.
}
double prevVelocity = osuPrevObj.JumpDistance / osuPrevObj.StrainTime; // do the same for the previous velocity.
if (osuLastObj.BaseObject is Slider)
{
double movementVelocity = osuPrevObj.MovementDistance / osuPrevObj.MovementTime;
double travelVelocity = osuPrevObj.TravelDistance / osuPrevObj.TravelTime;
prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity);
}
double angleBonus = 0;
double aimStrain = currVelocity; // Start strain with regular velocity.
if (Precision.AlmostEquals(osuCurrObj.StrainTime, osuPrevObj.StrainTime, 10)) // If rhythms are the same.
{
if (osuCurrObj.Angle != null && osuPrevObj.Angle != null)
{ {
double angle = osuCurrObj.Angle.Value; double currAngle = osuCurrObj.Angle.Value;
double prevAngle = osuPrevObj.Angle.Value;
// Rewarding angles, take the smaller velocity as base. // Rewarding angles, take the smaller velocity as base.
double angleBonus = Math.Min(currVector.Length, prevVector.Length); angleBonus = Math.Min(currVelocity, prevVelocity);
double wideAngleBonus = calcWideAngleBonus(angle); double wideAngleBonus = calcWideAngleBonus(currAngle);
double acuteAngleBonus = calcAcuteAngleBonus(angle); double acuteAngleBonus = calcAcuteAngleBonus(currAngle);
if (osuCurrObj.StrainTime > 100) if (osuCurrObj.StrainTime > 100) // Only buff deltaTime exceeding 300 bpm 1/2.
acuteAngleBonus = 0; acuteAngleBonus = 0;
else else
{ acuteAngleBonus *= calcAcuteAngleBonus(prevAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
acuteAngleBonus *= Math.Min(2, Math.Pow((100 - osuCurrObj.StrainTime) / 15, 1.5)); * Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
wideAngleBonus *= Math.Pow(osuCurrObj.StrainTime / 100, 6); * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4
} * Math.Pow(Math.Sin(Math.PI / 2 * (Math.Min(100, osuCurrObj.JumpDistance) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter).
if (acuteAngleBonus > wideAngleBonus) wideAngleBonus *= angleBonus * (1 - Math.Pow(calcWideAngleBonus(prevAngle), 3)); // Penalize wide angles if they're repeated, reducing the penalty as the prevAngle gets more acute.
angleBonus = Math.Min(angleBonus, 150 / osuCurrObj.StrainTime) * Math.Min(1, Math.Pow(Math.Min(osuCurrObj.JumpDistance, osuPrevObj.JumpDistance) / 150, 2));
angleBonus *= Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier); angleBonus = acuteAngleBonus * acute_angle_multiplier + wideAngleBonus * wide_angle_multiplier; // add the anglebuffs together.
// add in angle velocity.
aimStrain += angleBonus;
} }
} }
else // There is a rhythm change
{
// Rewarding rhythm, take the smaller velocity as base.
double rhythmBonus = Math.Min(currVector.Length, prevVector.Length);
if (osuCurrObj.StrainTime + 10 < osuPrevObj.StrainTime && osuPrevObj.StrainTime > osuLastObj.StrainTime + 10)
// Don't want to reward for a rhythm change back to back (unless its a double, which is why this only checks for fast -> slow -> fast).
rhythmBonus = 0;
aimStrain += rhythmBonus * rhythm_variance_multiplier; // add in rhythm velocity.
}
if (osuCurrObj.TravelDistance != 0) if (osuCurrObj.TravelDistance != 0)
{ {
double sliderBonus = (Math.Max(0, Vector2.Subtract(osuCurrObj.TravelVector, osuCurrObj.JumpVector).Length - osuCurrObj.JumpDistance) + osuCurrObj.TravelDistance) / osuCurrObj.StrainTime; sliderBonus = osuCurrObj.TravelDistance / osuCurrObj.TravelTime; // add some slider rewards
// Add in slider velocity.
aimStrain += sliderBonus * slider_multiplier;
} }
aimStrain += angleBonus; // Add in angle bonus.
aimStrain += sliderBonus * slider_multiplier; // Add in additional slider velocity.
return aimStrain; return aimStrain;
} }
private double calcWideAngleBonus(double angle) private double calcWideAngleBonus(double angle) => Math.Pow(Math.Sin(3.0 / 4 * (Math.Min(5.0 / 6 * Math.PI, Math.Max(Math.PI / 6, angle)) - Math.PI / 6)), 2);
private double calcAcuteAngleBonus(double angle) => 1 - calcWideAngleBonus(angle);
private double applyDiminishingExp(double val) => Math.Pow(val, 0.99);
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
protected override double CalculateInitialStrain(double time) => currentStrain * strainDecay(time - Previous[0].StartTime);
protected override double StrainValueAt(DifficultyHitObject current)
{ {
if (angle < Math.PI / 3) currentStrain *= strainDecay(current.DeltaTime);
return 0; currentStrain += strainValueOf(current) * skillMultiplier;
if (angle < 2 * Math.PI / 3)
return Math.Pow(Math.Sin(1.5 * (angle - Math.PI / 3)), 2);
return 0.25 + 0.75 * Math.Pow(Math.Sin(1.5 * (Math.PI - angle)), 2); return currentStrain;
}
private double calcAcuteAngleBonus(double angle)
{
if (angle < Math.PI / 3)
return 0.5 + 0.5 * Math.Pow(Math.Sin(1.5 * angle), 2);
if (angle < 2 * Math.PI / 3)
return Math.Pow(Math.Sin(1.5 * (2 * Math.PI / 3 - angle)), 2);
return 0;
} }
} }
} }

View File

@ -19,12 +19,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{ {
} }
protected override double SkillMultiplier => 0.15; private double skillMultiplier => 0.15;
protected override double StrainDecayBase => 0.15; private double strainDecayBase => 0.15;
protected override double DecayWeight => 1.0; protected override double DecayWeight => 1.0;
protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations. protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations.
private double currentStrain = 1;
protected override double StrainValueOf(DifficultyHitObject current) private double strainValueOf(DifficultyHitObject current)
{ {
if (current.BaseObject is Spinner) if (current.BaseObject is Spinner)
return 0; return 0;
@ -62,5 +63,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return Math.Pow(smallDistNerf * result, 2.0); return Math.Pow(smallDistNerf * result, 2.0);
} }
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
protected override double CalculateInitialStrain(double time) => currentStrain * strainDecay(time - Previous[0].StartTime);
protected override double StrainValueAt(DifficultyHitObject current)
{
currentStrain *= strainDecay(current.DeltaTime);
currentStrain += strainValueOf(current) * skillMultiplier;
return currentStrain;
}
} }
} }

View File

@ -10,7 +10,7 @@ using osu.Framework.Utils;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{ {
public abstract class OsuStrainSkill : StrainDecaySkill public abstract class OsuStrainSkill : StrainSkill
{ {
/// <summary> /// <summary>
/// The number of sections with the highest strains, which the peak strain reductions will apply to. /// The number of sections with the highest strains, which the peak strain reductions will apply to.

View File

@ -15,20 +15,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// </summary> /// </summary>
public class Speed : OsuStrainSkill public class Speed : OsuStrainSkill
{ {
private const double single_spacing_threshold = 135; private const double single_spacing_threshold = 125;
private const double rhythm_multiplier = 0.75;
private const double angle_bonus_begin = 5 * Math.PI / 6; private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max.
private const double pi_over_4 = Math.PI / 4;
private const double pi_over_2 = Math.PI / 2;
protected override double SkillMultiplier => 1400;
protected override double StrainDecayBase => 0.3;
protected override int ReducedSectionCount => 5;
protected override double DifficultyMultiplier => 1.04;
private const double min_speed_bonus = 75; // ~200BPM private const double min_speed_bonus = 75; // ~200BPM
private const double speed_balancing_factor = 40; private const double speed_balancing_factor = 40;
private double skillMultiplier => 1375;
private double strainDecayBase => 0.3;
private double currentStrain = 1;
private double currentRhythm = 1;
protected override int ReducedSectionCount => 5;
protected override double DifficultyMultiplier => 1.04;
protected override int HistoryLength => 32;
private readonly double greatWindow; private readonly double greatWindow;
public Speed(Mod[] mods, double hitWindowGreat) public Speed(Mod[] mods, double hitWindowGreat)
@ -37,52 +39,138 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
greatWindow = hitWindowGreat; greatWindow = hitWindowGreat;
} }
protected override double StrainValueOf(DifficultyHitObject current) /// <summary>
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
/// </summary>
private double calculateRhythmBonus(DifficultyHitObject current)
{ {
if (current.BaseObject is Spinner) if (current.BaseObject is Spinner)
return 0; return 0;
var osuCurrent = (OsuDifficultyHitObject)current; int previousIslandSize = 0;
var osuPrevious = Previous.Count > 0 ? (OsuDifficultyHitObject)Previous[0] : null;
double distance = Math.Min(single_spacing_threshold, osuCurrent.TravelDistance + osuCurrent.JumpDistance); double rhythmComplexitySum = 0;
double strainTime = osuCurrent.StrainTime; int islandSize = 1;
double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms
bool firstDeltaSwitch = false;
for (int i = Previous.Count - 2; i > 0; i--)
{
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)Previous[i - 1];
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)Previous[i];
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)Previous[i + 1];
double currHistoricalDecay = Math.Max(0, (history_time_max - (current.StartTime - currObj.StartTime))) / history_time_max; // scales note 0 to 1 from history to now
if (currHistoricalDecay != 0)
{
currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count.
double currDelta = currObj.StrainTime;
double prevDelta = prevObj.StrainTime;
double lastDelta = lastObj.StrainTime;
double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6));
windowPenalty = Math.Min(1, windowPenalty);
double effectiveRatio = windowPenalty * currRatio;
if (firstDeltaSwitch)
{
if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
{
if (islandSize < 7)
islandSize++; // island is still progressing, count size.
}
else
{
if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window
effectiveRatio *= 0.125;
if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
effectiveRatio *= 0.25;
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
effectiveRatio *= 0.25;
if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
effectiveRatio *= 0.50;
if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
effectiveRatio *= 0.125;
rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2;
startRatio = effectiveRatio;
previousIslandSize = islandSize; // log the last island size.
if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
islandSize = 1;
}
}
else if (prevDelta > 1.25 * currDelta) // we want to be speeding up.
{
// Begin counting island until we change speed again.
firstDeltaSwitch = true;
startRatio = effectiveRatio;
islandSize = 1;
}
}
}
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)
}
private double strainValueOf(DifficultyHitObject current)
{
if (current.BaseObject is Spinner)
return 0;
// derive strainTime for calculation
var osuCurrObj = (OsuDifficultyHitObject)current;
var osuPrevObj = Previous.Count > 0 ? (OsuDifficultyHitObject)Previous[0] : null;
double strainTime = osuCurrObj.StrainTime;
double greatWindowFull = greatWindow * 2; double greatWindowFull = greatWindow * 2;
double speedWindowRatio = strainTime / greatWindowFull; double speedWindowRatio = strainTime / greatWindowFull;
// Aim to nerf cheesy rhythms (Very fast consecutive doubles with large deltatimes between) // Aim to nerf cheesy rhythms (Very fast consecutive doubles with large deltatimes between)
if (osuPrevious != null && strainTime < greatWindowFull && osuPrevious.StrainTime > strainTime) if (osuPrevObj != null && strainTime < greatWindowFull && osuPrevObj.StrainTime > strainTime)
strainTime = Interpolation.Lerp(osuPrevious.StrainTime, strainTime, speedWindowRatio); strainTime = Interpolation.Lerp(osuPrevObj.StrainTime, strainTime, speedWindowRatio);
// Cap deltatime to the OD 300 hitwindow. // Cap deltatime to the OD 300 hitwindow.
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap. // 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
strainTime /= Math.Clamp((strainTime / greatWindowFull) / 0.93, 0.92, 1); strainTime /= Math.Clamp((strainTime / greatWindowFull) / 0.93, 0.92, 1);
// derive speedBonus for calculation
double speedBonus = 1.0; double speedBonus = 1.0;
if (strainTime < min_speed_bonus) if (strainTime < min_speed_bonus)
speedBonus = 1 + Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
double angleBonus = 1.0; double distance = Math.Min(single_spacing_threshold, osuCurrObj.TravelDistance + osuCurrObj.JumpDistance);
if (osuCurrent.Angle != null && osuCurrent.Angle.Value < angle_bonus_begin) return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime;
{ }
angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - osuCurrent.Angle.Value)), 2) / 3.57;
if (osuCurrent.Angle.Value < pi_over_2) private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
{
angleBonus = 1.28;
if (distance < 90 && osuCurrent.Angle.Value < pi_over_4)
angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1);
else if (distance < 90)
angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1) * Math.Sin((pi_over_2 - osuCurrent.Angle.Value) / pi_over_4);
}
}
return (1 + (speedBonus - 1) * 0.75) protected override double CalculateInitialStrain(double time) => (currentStrain * currentRhythm) * strainDecay(time - Previous[0].StartTime);
* angleBonus
* (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.0)) protected override double StrainValueAt(DifficultyHitObject current)
/ strainTime; {
currentStrain *= strainDecay(current.DeltaTime);
currentStrain += strainValueOf(current) * skillMultiplier;
currentRhythm = calculateRhythmBonus(current);
return currentStrain * currentRhythm;
} }
} }
} }

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks
public IEnumerable<Issue> Run(BeatmapVerifierContext context) public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{ {
double od = context.Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty; double od = context.Beatmap.Difficulty.OverallDifficulty;
// These are meant to reflect the duration necessary for auto to score at least 1000 points on the spinner. // These are meant to reflect the duration necessary for auto to score at least 1000 points on the spinner.
// It's difficult to eliminate warnings here, as auto achieving 1000 points depends on the approach angle on some spinners. // It's difficult to eliminate warnings here, as auto achieving 1000 points depends on the approach angle on some spinners.

View File

@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Mods
#region Reduce AR (IApplicableToDifficulty) #region Reduce AR (IApplicableToDifficulty)
public void ReadFromDifficulty(BeatmapDifficulty difficulty) public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty)
{ {
} }
@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Osu.Mods
.Select(beat => .Select(beat =>
{ {
var newCircle = new HitCircle(); var newCircle = new HitCircle();
newCircle.ApplyDefaults(controlPointInfo, osuBeatmap.BeatmapInfo.BaseDifficulty); newCircle.ApplyDefaults(controlPointInfo, osuBeatmap.Difficulty);
newCircle.StartTime = beat; newCircle.StartTime = beat;
return (OsuHitObject)newCircle; return (OsuHitObject)newCircle;
}).ToList(); }).ToList();

View File

@ -122,11 +122,11 @@ namespace osu.Game.Rulesets.Osu.Objects
}); });
} }
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN); TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN);
// Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR. // Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR.
// This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above. // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above.

View File

@ -79,6 +79,12 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary> /// </summary>
internal float LazyTravelDistance; internal float LazyTravelDistance;
/// <summary>
/// The time taken by the cursor upon completion of this <see cref="Slider"/> if it was hit
/// with as few movements as possible. This is set and used by difficulty calculation.
/// </summary>
internal double LazyTravelTime;
public List<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>(); public List<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>();
[JsonIgnore] [JsonIgnore]
@ -135,7 +141,7 @@ namespace osu.Game.Rulesets.Osu.Objects
Path.Version.ValueChanged += _ => updateNestedPositions(); Path.Version.ValueChanged += _ => updateNestedPositions();
} }
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);

View File

@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public double SpanDuration => slider.SpanDuration; public double SpanDuration => slider.SpanDuration;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public int SpanIndex { get; set; } public int SpanIndex { get; set; }
public double SpanStartTime { get; set; } public double SpanStartTime { get; set; }
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);

View File

@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary> /// </summary>
public int MaximumBonusSpins { get; protected set; } = 1; public int MaximumBonusSpins { get; protected set; } = 1;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Objects
double secondsDuration = Duration / 1000; double secondsDuration = Duration / 1000;
double minimumRotationsPerSecond = stable_matching_fudge * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5); double minimumRotationsPerSecond = stable_matching_fudge * IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5);
SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond); SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond);
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration); MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration);

View File

@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Replays
: base(beatmap, mods) : base(beatmap, mods)
{ {
defaultHitWindows = new OsuHitWindows(); defaultHitWindows = new OsuHitWindows();
defaultHitWindows.SetDifficulty(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); defaultHitWindows.SetDifficulty(Beatmap.Difficulty.OverallDifficulty);
} }
#endregion #endregion

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private OsuPlayfield playfield { get; set; } private OsuPlayfield playfield { get; set; }
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private GameplayBeatmap gameplayBeatmap { get; set; } private GameplayState gameplayState { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skin, OsuColour colours) private void load(ISkinSource skin, OsuColour colours)
@ -75,12 +75,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
protected override void Update() protected override void Update()
{ {
if (playfield == null || gameplayBeatmap == null) return; if (playfield == null || gameplayState == null) return;
DrawableHitObject kiaiHitObject = null; DrawableHitObject kiaiHitObject = null;
// Check whether currently in a kiai section first. This is only done as an optimisation to avoid enumerating AliveObjects when not necessary. // Check whether currently in a kiai section first. This is only done as an optimisation to avoid enumerating AliveObjects when not necessary.
if (gameplayBeatmap.ControlPointInfo.EffectPointAt(Time.Current).KiaiMode) if (gameplayState.Beatmap.ControlPointInfo.EffectPointAt(Time.Current).KiaiMode)
kiaiHitObject = playfield.HitObjectContainer.AliveObjects.FirstOrDefault(isTracking); kiaiHitObject = playfield.HitObjectContainer.AliveObjects.FirstOrDefault(isTracking);
kiaiSpewer.Active.Value = kiaiHitObject != null; kiaiSpewer.Active.Value = kiaiHitObject != null;

View File

@ -35,8 +35,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private Drawable hitCircleSprite; private Drawable hitCircleSprite;
protected Drawable HitCircleOverlay { get; private set; } protected Container OverlayLayer { get; private set; }
private Drawable hitCircleOverlay;
private SkinnableSpriteText hitCircleText; private SkinnableSpriteText hitCircleText;
private readonly Bindable<Color4> accentColour = new Bindable<Color4>(); private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
@ -78,17 +79,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, },
HitCircleOverlay = new KiaiFlashingSprite OverlayLayer = new Container
{ {
Texture = overlayTexture,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, Child = hitCircleOverlay = new KiaiFlashingSprite
{
Texture = overlayTexture,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
}
}; };
if (hasNumber) if (hasNumber)
{ {
AddInternal(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText OverlayLayer.Add(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
{ {
Font = OsuFont.Numeric.With(size: 40), Font = OsuFont.Numeric.With(size: 40),
UseFullGlyphHeight = false, UseFullGlyphHeight = false,
@ -102,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
if (overlayAboveNumber) if (overlayAboveNumber)
ChangeInternalChildDepth(HitCircleOverlay, float.MinValue); OverlayLayer.ChangeChildDepth(hitCircleOverlay, float.MinValue);
accentColour.BindTo(drawableObject.AccentColour); accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
@ -147,8 +153,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
hitCircleSprite.FadeOut(legacy_fade_duration, Easing.Out); hitCircleSprite.FadeOut(legacy_fade_duration, Easing.Out);
hitCircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); hitCircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
HitCircleOverlay.FadeOut(legacy_fade_duration, Easing.Out); hitCircleOverlay.FadeOut(legacy_fade_duration, Easing.Out);
HitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); hitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
if (hasNumber) if (hasNumber)
{ {

View File

@ -13,26 +13,20 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
public class LegacyReverseArrow : CompositeDrawable public class LegacyReverseArrow : CompositeDrawable
{ {
private ISkin skin { get; }
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private DrawableHitObject drawableHitObject { get; set; } private DrawableHitObject drawableHitObject { get; set; }
private Drawable proxy; private Drawable proxy;
public LegacyReverseArrow(ISkin skin)
{
this.skin = skin;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(ISkinSource skinSource)
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
string lookupName = new OsuSkinComponent(OsuSkinComponents.ReverseArrow).LookupName; string lookupName = new OsuSkinComponent(OsuSkinComponents.ReverseArrow).LookupName;
InternalChild = skin.GetAnimation(lookupName, true, true) ?? Empty(); var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null);
InternalChild = skin?.GetAnimation(lookupName, true, true) ?? Empty();
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private DrawableHitObject drawableHitObject { get; set; } private DrawableHitObject drawableHitObject { get; set; }
private Drawable proxiedHitCircleOverlay; private Drawable proxiedOverlayLayer;
public LegacySliderHeadHitCircle() public LegacySliderHeadHitCircle()
: base("sliderstartcircle") : base("sliderstartcircle")
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
proxiedHitCircleOverlay = HitCircleOverlay.CreateProxy(); proxiedOverlayLayer = OverlayLayer.CreateProxy();
if (drawableHitObject != null) if (drawableHitObject != null)
{ {
@ -35,11 +35,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private void onHitObjectApplied(DrawableHitObject drawableObject) private void onHitObjectApplied(DrawableHitObject drawableObject)
{ {
Debug.Assert(proxiedHitCircleOverlay.Parent == null); Debug.Assert(proxiedOverlayLayer.Parent == null);
// see logic in LegacyReverseArrow. // see logic in LegacyReverseArrow.
(drawableObject as DrawableSliderHead)?.DrawableSlider (drawableObject as DrawableSliderHead)?.DrawableSlider
.OverlayElementContainer.Add(proxiedHitCircleOverlay.With(d => d.Depth = float.MinValue)); .OverlayElementContainer.Add(proxiedOverlayLayer.With(d => d.Depth = float.MinValue));
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
case OsuSkinComponents.ReverseArrow: case OsuSkinComponents.ReverseArrow:
if (hasHitCircle.Value) if (hasHitCircle.Value)
return new LegacyReverseArrow(this); return new LegacyReverseArrow();
return null; return null;

View File

@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
return; return;
// Todo: This should probably not be done like this. // Todo: This should probably not be done like this.
float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5) / 5) / 2; float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.Difficulty.CircleSize - 5) / 5) / 2;
foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle))) foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)))
{ {

View File

@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
} }
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private GameplayBeatmap beatmap { get; set; } private GameplayState state { get; set; }
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } private OsuConfigManager config { get; set; }
@ -96,10 +96,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{ {
float scale = userCursorScale.Value; float scale = userCursorScale.Value;
if (autoCursorScale.Value && beatmap != null) if (autoCursorScale.Value && state != null)
{ {
// if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier. // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
scale *= GetScaleForCircleSize(beatmap.BeatmapInfo.BaseDifficulty.CircleSize); scale *= GetScaleForCircleSize(state.Beatmap.Difficulty.CircleSize);
} }
cursorScale.Value = scale; cursorScale.Value = scale;

View File

@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
[TestCase(2.2867022617692685d, "diffcalc-test")] [TestCase(2.2420075288523802d, "diffcalc-test")]
[TestCase(2.2867022617692685d, "diffcalc-test-strong")] [TestCase(2.2420075288523802d, "diffcalc-test-strong")]
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, name); => base.Test(expected, name);
[TestCase(3.1704781712282624d, "diffcalc-test")] [TestCase(3.134084469440479d, "diffcalc-test")]
[TestCase(3.1704781712282624d, "diffcalc-test-strong")] [TestCase(3.134084469440479d, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new TaikoModDoubleTime()); => Test(expected, name, new TaikoModDoubleTime());

View File

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Utils; using osu.Framework.Utils;
using System.Threading; using System.Threading;
using JetBrains.Annotations;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
@ -46,11 +47,10 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
protected override Beatmap<TaikoHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) protected override Beatmap<TaikoHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{ {
if (!(original.BeatmapInfo.BaseDifficulty is TaikoMutliplierAppliedDifficulty)) if (!(original.Difficulty is TaikoMultiplierAppliedDifficulty))
{ {
// Rewrite the beatmap info to add the slider velocity multiplier // Rewrite the beatmap info to add the slider velocity multiplier
original.BeatmapInfo = original.BeatmapInfo.Clone(); original.Difficulty = new TaikoMultiplierAppliedDifficulty(original.Difficulty);
original.BeatmapInfo.BaseDifficulty = new TaikoMutliplierAppliedDifficulty(original.BeatmapInfo.BaseDifficulty);
} }
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original, cancellationToken); Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original, cancellationToken);
@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
StartTime = obj.StartTime, StartTime = obj.StartTime,
Samples = obj.Samples, Samples = obj.Samples,
Duration = taikoDuration, Duration = taikoDuration,
TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4 TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4
}; };
} }
@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
case IHasDuration endTimeData: case IHasDuration endTimeData:
{ {
double hitMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier; double hitMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier;
yield return new Swell yield return new Swell
{ {
@ -164,10 +164,10 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
else else
beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier; beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate; double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate;
// The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll. // The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll.
double taikoVelocity = sliderScoringPointDistance * beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate; double taikoVelocity = sliderScoringPointDistance * beatmap.Difficulty.SliderTickRate;
taikoDuration = (int)(distance / taikoVelocity * beatLength); taikoDuration = (int)(distance / taikoVelocity * beatLength);
if (isForCurrentRuleset) if (isForCurrentRuleset)
@ -183,7 +183,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
beatLength = timingPoint.BeatLength; beatLength = timingPoint.BeatLength;
// If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat // If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
tickSpacing = Math.Min(beatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, (double)taikoDuration / spans); tickSpacing = Math.Min(beatLength / beatmap.Difficulty.SliderTickRate, (double)taikoDuration / spans);
return tickSpacing > 0 return tickSpacing > 0
&& distance / osuVelocity * 1000 < 2 * beatLength; && distance / osuVelocity * 1000 < 2 * beatLength;
@ -191,13 +191,35 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
protected override Beatmap<TaikoHitObject> CreateBeatmap() => new TaikoBeatmap(); protected override Beatmap<TaikoHitObject> CreateBeatmap() => new TaikoBeatmap();
private class TaikoMutliplierAppliedDifficulty : BeatmapDifficulty private class TaikoMultiplierAppliedDifficulty : BeatmapDifficulty
{ {
public TaikoMutliplierAppliedDifficulty(BeatmapDifficulty difficulty) public TaikoMultiplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty)
{ {
difficulty.CopyTo(this); CopyFrom(difficulty);
SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
} }
[UsedImplicitly]
public TaikoMultiplierAppliedDifficulty()
{
}
#region Overrides of BeatmapDifficulty
public override void CopyTo(BeatmapDifficulty other)
{
base.CopyTo(other);
if (!(other is TaikoMultiplierAppliedDifficulty))
SliderMultiplier /= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
}
public override void CopyFrom(IBeatmapDifficultyInfo other)
{
base.CopyFrom(other);
if (!(other is TaikoMultiplierAppliedDifficulty))
SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
}
#endregion
} }
} }
} }

View File

@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
starRating = rescale(starRating); starRating = rescale(starRating);
HitWindows hitWindows = new TaikoHitWindows(); HitWindows hitWindows = new TaikoHitWindows();
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
return new TaikoDifficultyAttributes return new TaikoDifficultyAttributes
{ {
@ -94,8 +94,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
StaminaStrain = staminaRating, StaminaStrain = staminaRating,
RhythmStrain = rhythmRating, RhythmStrain = rhythmRating,
ColourStrain = colourRating, ColourStrain = colourRating,
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
GreatHitWindow = (int)hitWindows.WindowFor(HitResult.Great) / clockRate,
MaxCombo = beatmap.HitObjects.Count(h => h is Hit), MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
Skills = skills Skills = skills
}; };

View File

@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
private float overallDifficulty = BeatmapDifficulty.DEFAULT_DIFFICULTY; private float overallDifficulty = BeatmapDifficulty.DEFAULT_DIFFICULTY;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);

View File

@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Taiko.Scoring
{ {
base.ApplyBeatmap(beatmap); base.ApplyBeatmap(beatmap);
hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType<Hit>().Count()) * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType<Hit>().Count()) * IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.5, 0.75, 0.98));
hpMissMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120); hpMissMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.0018, 0.0075, 0.0120);
} }
protected override double GetHealthIncreaseFor(JudgementResult result) protected override double GetHealthIncreaseFor(JudgementResult result)

View File

@ -25,10 +25,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(GameplayBeatmap gameplayBeatmap) private void load(GameplayState gameplayState)
{ {
if (gameplayBeatmap != null) if (gameplayState != null)
((IBindable<JudgementResult>)LastResult).BindTo(gameplayBeatmap.LastJudgementResult); ((IBindable<JudgementResult>)LastResult).BindTo(gameplayState.LastJudgementResult);
} }
private bool passing; private bool passing;

View File

@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.UI
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(TextureStore textures, GameplayBeatmap gameplayBeatmap) private void load(TextureStore textures, GameplayState gameplayState)
{ {
InternalChildren = new[] InternalChildren = new[]
{ {
@ -49,8 +49,8 @@ namespace osu.Game.Rulesets.Taiko.UI
animations[TaikoMascotAnimationState.Fail] = new TaikoMascotAnimation(TaikoMascotAnimationState.Fail), animations[TaikoMascotAnimationState.Fail] = new TaikoMascotAnimation(TaikoMascotAnimationState.Fail),
}; };
if (gameplayBeatmap != null) if (gameplayState != null)
((IBindable<JudgementResult>)LastResult).BindTo(gameplayBeatmap.LastJudgementResult); ((IBindable<JudgementResult>)LastResult).BindTo(gameplayState.LastJudgementResult);
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -129,7 +129,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
{ {
var difficulty = decoder.Decode(stream).BeatmapInfo.BaseDifficulty; var difficulty = decoder.Decode(stream).Difficulty;
Assert.AreEqual(6.5f, difficulty.DrainRate); Assert.AreEqual(6.5f, difficulty.DrainRate);
Assert.AreEqual(4, difficulty.CircleSize); Assert.AreEqual(4, difficulty.CircleSize);

View File

@ -149,5 +149,32 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual(AnimationLoopType.LoopForever, ((StoryboardAnimation)foreground.Elements[5]).LoopType); Assert.AreEqual(AnimationLoopType.LoopForever, ((StoryboardAnimation)foreground.Elements[5]).LoopType);
} }
} }
[Test]
public void TestDecodeLoopCount()
{
// all loop sequences in loop-count.osb have a total duration of 2000ms (fade in 0->1000ms, fade out 1000->2000ms).
const double loop_duration = 2000;
var decoder = new LegacyStoryboardDecoder();
using (var resStream = TestResources.OpenResource("loop-count.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var storyboard = decoder.Decode(stream);
StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
// stable ensures that any loop command executes at least once, even if the loop count specified in the .osb is zero or negative.
StoryboardSprite zeroTimes = background.Elements.OfType<StoryboardSprite>().Single(s => s.Path == "zero-times.png");
Assert.That(zeroTimes.EndTime, Is.EqualTo(1000 + loop_duration));
StoryboardSprite oneTime = background.Elements.OfType<StoryboardSprite>().Single(s => s.Path == "one-time.png");
Assert.That(oneTime.EndTime, Is.EqualTo(4000 + loop_duration));
StoryboardSprite manyTimes = background.Elements.OfType<StoryboardSprite>().Single(s => s.Path == "many-times.png");
Assert.That(manyTimes.EndTime, Is.EqualTo(9000 + 40 * loop_duration));
}
}
} }
} }

View File

@ -84,7 +84,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
public void TestDecodeDifficulty() public void TestDecodeDifficulty()
{ {
var beatmap = decodeAsJson(normal); var beatmap = decodeAsJson(normal);
var difficulty = beatmap.BeatmapInfo.BaseDifficulty; var difficulty = beatmap.Difficulty;
Assert.AreEqual(6.5f, difficulty.DrainRate); Assert.AreEqual(6.5f, difficulty.DrainRate);
Assert.AreEqual(4, difficulty.CircleSize); Assert.AreEqual(4, difficulty.CircleSize);
Assert.AreEqual(8, difficulty.OverallDifficulty); Assert.AreEqual(8, difficulty.OverallDifficulty);
@ -102,7 +102,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
processor.PreProcess(); processor.PreProcess();
foreach (var o in converted.HitObjects) foreach (var o in converted.HitObjects)
o.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty); o.ApplyDefaults(converted.ControlPointInfo, converted.Difficulty);
processor.PostProcess(); processor.PostProcess();
var beatmap = converted.Serialize().Deserialize<Beatmap>(); var beatmap = converted.Serialize().Deserialize<Beatmap>();

View File

@ -86,7 +86,7 @@ namespace osu.Game.Tests.Beatmaps.IO
var manager = osu.Dependencies.Get<BeatmapManager>(); var manager = osu.Dependencies.Get<BeatmapManager>();
BeatmapSetInfo importedSet; ILive<BeatmapSetInfo> importedSet;
using (var stream = File.OpenRead(tempPath)) using (var stream = File.OpenRead(tempPath))
{ {
@ -97,7 +97,7 @@ namespace osu.Game.Tests.Beatmaps.IO
Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing"); Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing");
File.Delete(tempPath); File.Delete(tempPath);
var imported = manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID); var imported = manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID);
deleteBeatmapSet(imported, osu); deleteBeatmapSet(imported, osu);
} }
@ -172,8 +172,8 @@ namespace osu.Game.Tests.Beatmaps.IO
ensureLoaded(osu); ensureLoaded(osu);
// but contents doesn't, so existing should still be used. // but contents doesn't, so existing should still be used.
Assert.IsTrue(imported.ID == importedSecondTime.ID); Assert.IsTrue(imported.ID == importedSecondTime.Value.ID);
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Value.Beatmaps.First().ID);
} }
finally finally
{ {
@ -226,8 +226,8 @@ namespace osu.Game.Tests.Beatmaps.IO
ensureLoaded(osu); ensureLoaded(osu);
// check the newly "imported" beatmap is not the original. // check the newly "imported" beatmap is not the original.
Assert.IsTrue(imported.ID != importedSecondTime.ID); Assert.IsTrue(imported.ID != importedSecondTime.Value.ID);
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID); Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID);
} }
finally finally
{ {
@ -278,8 +278,8 @@ namespace osu.Game.Tests.Beatmaps.IO
ensureLoaded(osu); ensureLoaded(osu);
// check the newly "imported" beatmap is not the original. // check the newly "imported" beatmap is not the original.
Assert.IsTrue(imported.ID != importedSecondTime.ID); Assert.IsTrue(imported.ID != importedSecondTime.Value.ID);
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID); Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID);
} }
finally finally
{ {
@ -329,8 +329,8 @@ namespace osu.Game.Tests.Beatmaps.IO
ensureLoaded(osu); ensureLoaded(osu);
// check the newly "imported" beatmap is not the original. // check the newly "imported" beatmap is not the original.
Assert.IsTrue(imported.ID != importedSecondTime.ID); Assert.IsTrue(imported.ID != importedSecondTime.Value.ID);
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID); Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID);
} }
finally finally
{ {
@ -570,8 +570,8 @@ namespace osu.Game.Tests.Beatmaps.IO
var imported = await manager.Import(toImport); var imported = await manager.Import(toImport);
Assert.NotNull(imported); Assert.NotNull(imported);
Assert.AreEqual(null, imported.Beatmaps[0].OnlineBeatmapID); Assert.AreEqual(null, imported.Value.Beatmaps[0].OnlineBeatmapID);
Assert.AreEqual(null, imported.Beatmaps[1].OnlineBeatmapID); Assert.AreEqual(null, imported.Value.Beatmaps[1].OnlineBeatmapID);
} }
finally finally
{ {
@ -582,7 +582,6 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test] [Test]
[NonParallelizable] [NonParallelizable]
[Ignore("Binding IPC on Appveyor isn't working (port in use). Need to figure out why")]
public void TestImportOverIPC() public void TestImportOverIPC()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-host", true)) using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-host", true))
@ -707,7 +706,7 @@ namespace osu.Game.Tests.Beatmaps.IO
ensureLoaded(osu); ensureLoaded(osu);
Assert.IsFalse(imported.Files.Any(f => f.Filename.Contains("subfolder")), "Files contain common subfolder"); Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("subfolder")), "Files contain common subfolder");
} }
finally finally
{ {
@ -760,8 +759,8 @@ namespace osu.Game.Tests.Beatmaps.IO
ensureLoaded(osu); ensureLoaded(osu);
Assert.IsFalse(imported.Files.Any(f => f.Filename.Contains("__MACOSX")), "Files contain resource fork folder, which should be ignored"); Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("__MACOSX")), "Files contain resource fork folder, which should be ignored");
Assert.IsFalse(imported.Files.Any(f => f.Filename.Contains("actual_data")), "Files contain common subfolder"); Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("actual_data")), "Files contain common subfolder");
} }
finally finally
{ {
@ -910,13 +909,13 @@ namespace osu.Game.Tests.Beatmaps.IO
var manager = osu.Dependencies.Get<BeatmapManager>(); var manager = osu.Dependencies.Get<BeatmapManager>();
var importedSet = await manager.Import(new ImportTask(temp)); var importedSet = await manager.Import(new ImportTask(temp)).ConfigureAwait(false);
ensureLoaded(osu); ensureLoaded(osu);
waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID); return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID);
} }
public static async Task<BeatmapSetInfo> LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false) public static async Task<BeatmapSetInfo> LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false)
@ -925,13 +924,13 @@ namespace osu.Game.Tests.Beatmaps.IO
var manager = osu.Dependencies.Get<BeatmapManager>(); var manager = osu.Dependencies.Get<BeatmapManager>();
var importedSet = await manager.Import(new ImportTask(temp)); var importedSet = await manager.Import(new ImportTask(temp)).ConfigureAwait(false);
ensureLoaded(osu); ensureLoaded(osu);
waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID); return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID);
} }
private void deleteBeatmapSet(BeatmapSetInfo imported, OsuGameBase osu) private void deleteBeatmapSet(BeatmapSetInfo imported, OsuGameBase osu)
@ -946,13 +945,13 @@ namespace osu.Game.Tests.Beatmaps.IO
Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending);
} }
private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmap) private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmapInfo)
{ {
return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
{ {
OnlineScoreID = 2, OnlineScoreID = 2,
Beatmap = beatmap, BeatmapInfo = beatmapInfo,
BeatmapInfoID = beatmap.ID BeatmapInfoID = beatmapInfo.ID
}, new ImportScoreTest.TestArchiveReader()); }, new ImportScoreTest.TestArchiveReader());
} }

View File

@ -509,5 +509,17 @@ namespace osu.Game.Tests.Chat
Assert.AreEqual(LinkAction.External, result.Action); Assert.AreEqual(LinkAction.External, result.Action);
Assert.AreEqual("/relative", result.Argument); Assert.AreEqual("/relative", result.Argument);
} }
[TestCase("https://dev.ppy.sh/home/changelog", "")]
[TestCase("https://dev.ppy.sh/home/changelog/lazer/2021.1012", "lazer/2021.1012")]
public void TestChangelogLinks(string link, string expectedArg)
{
MessageFormatter.WebsiteRootUrl = "dev.ppy.sh";
LinkDetails result = MessageFormatter.GetLinkDetails(link);
Assert.AreEqual(LinkAction.OpenChangelog, result.Action);
Assert.AreEqual(expectedArg, result.Argument);
}
} }
} }

View File

@ -0,0 +1,114 @@
// 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.Diagnostics;
using System.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Logging;
using osu.Game.Models;
using osu.Game.Stores;
#nullable enable
namespace osu.Game.Tests.Database
{
public class FileStoreTests : RealmTest
{
[Test]
public void TestImportFile()
{
RunTestWithRealm((realmFactory, storage) =>
{
var realm = realmFactory.Context;
var files = new RealmFileStore(realmFactory, storage);
var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 });
realm.Write(() => files.Add(testData, realm));
Assert.True(files.Storage.Exists("0/05/054edec1d0211f624fed0cbca9d4f9400b0e491c43742af2c5b0abebf0c990d8"));
Assert.True(files.Storage.Exists(realm.All<RealmFile>().First().StoragePath));
});
}
[Test]
public void TestImportSameFileTwice()
{
RunTestWithRealm((realmFactory, storage) =>
{
var realm = realmFactory.Context;
var files = new RealmFileStore(realmFactory, storage);
var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 });
realm.Write(() => files.Add(testData, realm));
realm.Write(() => files.Add(testData, realm));
Assert.AreEqual(1, realm.All<RealmFile>().Count());
});
}
[Test]
public void TestDontPurgeReferenced()
{
RunTestWithRealm((realmFactory, storage) =>
{
var realm = realmFactory.Context;
var files = new RealmFileStore(realmFactory, storage);
var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm));
var timer = new Stopwatch();
timer.Start();
realm.Write(() =>
{
// attach the file to an arbitrary beatmap
var beatmapSet = CreateBeatmapSet(CreateRuleset());
beatmapSet.Files.Add(new RealmNamedFileUsage(file, "arbitrary.resource"));
realm.Add(beatmapSet);
});
Logger.Log($"Import complete at {timer.ElapsedMilliseconds}");
string path = file.StoragePath;
Assert.True(realm.All<RealmFile>().Any());
Assert.True(files.Storage.Exists(path));
files.Cleanup();
Logger.Log($"Cleanup complete at {timer.ElapsedMilliseconds}");
Assert.True(realm.All<RealmFile>().Any());
Assert.True(file.IsValid);
Assert.True(files.Storage.Exists(path));
});
}
[Test]
public void TestPurgeUnreferenced()
{
RunTestWithRealm((realmFactory, storage) =>
{
var realm = realmFactory.Context;
var files = new RealmFileStore(realmFactory, storage);
var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm));
string path = file.StoragePath;
Assert.True(realm.All<RealmFile>().Any());
Assert.True(files.Storage.Exists(path));
files.Cleanup();
Assert.False(realm.All<RealmFile>().Any());
Assert.False(file.IsValid);
Assert.False(files.Storage.Exists(path));
});
}
}
}

View File

@ -0,0 +1,67 @@
// 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.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
#nullable enable
namespace osu.Game.Tests.Database
{
[TestFixture]
public class GeneralUsageTests : RealmTest
{
/// <summary>
/// Just test the construction of a new database works.
/// </summary>
[Test]
public void TestConstructRealm()
{
RunTestWithRealm((realmFactory, _) => { realmFactory.CreateContext().Refresh(); });
}
[Test]
public void TestBlockOperations()
{
RunTestWithRealm((realmFactory, _) =>
{
using (realmFactory.BlockAllOperations())
{
}
});
}
[Test]
public void TestBlockOperationsWithContention()
{
RunTestWithRealm((realmFactory, _) =>
{
ManualResetEventSlim stopThreadedUsage = new ManualResetEventSlim();
ManualResetEventSlim hasThreadedUsage = new ManualResetEventSlim();
Task.Factory.StartNew(() =>
{
using (realmFactory.CreateContext())
{
hasThreadedUsage.Set();
stopThreadedUsage.Wait();
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler);
hasThreadedUsage.Wait();
Assert.Throws<TimeoutException>(() =>
{
using (realmFactory.BlockAllOperations())
{
}
});
stopThreadedUsage.Set();
});
}
}
}

View File

@ -0,0 +1,213 @@
// 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.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Models;
using Realms;
#nullable enable
namespace osu.Game.Tests.Database
{
public class RealmLiveTests : RealmTest
{
[Test]
public void TestLiveCastability()
{
RunTestWithRealm((realmFactory, _) =>
{
RealmLive<RealmBeatmap> beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive();
ILive<IBeatmapInfo> iBeatmap = beatmap;
Assert.AreEqual(0, iBeatmap.Value.Length);
});
}
[Test]
public void TestValueAccessWithOpenContext()
{
RunTestWithRealm((realmFactory, _) =>
{
RealmLive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
Debug.Assert(liveBeatmap != null);
Task.Factory.StartNew(() =>
{
Assert.DoesNotThrow(() =>
{
using (realmFactory.CreateContext())
{
var resolved = liveBeatmap.Value;
Assert.IsTrue(resolved.Realm.IsClosed);
Assert.IsTrue(resolved.IsValid);
// can access properties without a crash.
Assert.IsFalse(resolved.Hidden);
}
});
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
});
}
[Test]
public void TestScopedReadWithoutContext()
{
RunTestWithRealm((realmFactory, _) =>
{
RealmLive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
Debug.Assert(liveBeatmap != null);
Task.Factory.StartNew(() =>
{
liveBeatmap.PerformRead(beatmap =>
{
Assert.IsTrue(beatmap.IsValid);
Assert.IsFalse(beatmap.Hidden);
});
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
});
}
[Test]
public void TestScopedWriteWithoutContext()
{
RunTestWithRealm((realmFactory, _) =>
{
RealmLive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
Debug.Assert(liveBeatmap != null);
Task.Factory.StartNew(() =>
{
liveBeatmap.PerformWrite(beatmap => { beatmap.Hidden = true; });
liveBeatmap.PerformRead(beatmap => { Assert.IsTrue(beatmap.Hidden); });
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
});
}
[Test]
public void TestValueAccessWithoutOpenContextFails()
{
RunTestWithRealm((realmFactory, _) =>
{
RealmLive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
Debug.Assert(liveBeatmap != null);
Task.Factory.StartNew(() =>
{
Assert.Throws<InvalidOperationException>(() =>
{
var unused = liveBeatmap.Value;
});
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
});
}
[Test]
public void TestLiveAssumptions()
{
RunTestWithRealm((realmFactory, _) =>
{
int changesTriggered = 0;
using (var updateThreadContext = realmFactory.CreateContext())
{
updateThreadContext.All<RealmBeatmap>().SubscribeForNotifications(gotChange);
RealmLive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
{
var ruleset = CreateRuleset();
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
// add a second beatmap to ensure that a full refresh occurs below.
// not just a refresh from the resolved Live.
threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
Debug.Assert(liveBeatmap != null);
// not yet seen by main context
Assert.AreEqual(0, updateThreadContext.All<RealmBeatmap>().Count());
Assert.AreEqual(0, changesTriggered);
var resolved = liveBeatmap.Value;
// retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
Assert.AreEqual(2, updateThreadContext.All<RealmBeatmap>().Count());
Assert.AreEqual(1, changesTriggered);
// even though the realm that this instance was resolved for was closed, it's still valid.
Assert.IsTrue(resolved.Realm.IsClosed);
Assert.IsTrue(resolved.IsValid);
// can access properties without a crash.
Assert.IsFalse(resolved.Hidden);
updateThreadContext.Write(r =>
{
// can use with the main context.
r.Remove(resolved);
});
}
void gotChange(IRealmCollection<RealmBeatmap> sender, ChangeSet changes, Exception error)
{
changesTriggered++;
}
});
}
}
}

View File

@ -0,0 +1,151 @@
// 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.Runtime.CompilerServices;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Models;
#nullable enable
namespace osu.Game.Tests.Database
{
[TestFixture]
public abstract class RealmTest
{
private static readonly TemporaryNativeStorage storage;
static RealmTest()
{
storage = new TemporaryNativeStorage("realm-test");
storage.DeleteDirectory(string.Empty);
}
protected void RunTestWithRealm(Action<RealmContextFactory, Storage> testAction, [CallerMemberName] string caller = "")
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller))
{
host.Run(new RealmTestGame(() =>
{
var testStorage = storage.GetStorageForDirectory(caller);
using (var realmFactory = new RealmContextFactory(testStorage, caller))
{
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
testAction(realmFactory, testStorage);
realmFactory.Dispose();
Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
realmFactory.Compact();
Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}");
}
}));
}
}
protected void RunTestWithRealmAsync(Func<RealmContextFactory, Storage, Task> testAction, [CallerMemberName] string caller = "")
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller))
{
host.Run(new RealmTestGame(async () =>
{
var testStorage = storage.GetStorageForDirectory(caller);
using (var realmFactory = new RealmContextFactory(testStorage, caller))
{
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
await testAction(realmFactory, testStorage);
realmFactory.Dispose();
Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
realmFactory.Compact();
}
}));
}
}
protected static RealmBeatmapSet CreateBeatmapSet(RealmRuleset ruleset)
{
RealmFile createRealmFile() => new RealmFile { Hash = Guid.NewGuid().ToString().ComputeSHA2Hash() };
var metadata = new RealmBeatmapMetadata
{
Title = "My Love",
Artist = "Kuba Oms"
};
var beatmapSet = new RealmBeatmapSet
{
Beatmaps =
{
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Easy", },
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Normal", },
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Hard", },
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Insane", }
},
Files =
{
new RealmNamedFileUsage(createRealmFile(), "test [easy].osu"),
new RealmNamedFileUsage(createRealmFile(), "test [normal].osu"),
new RealmNamedFileUsage(createRealmFile(), "test [hard].osu"),
new RealmNamedFileUsage(createRealmFile(), "test [insane].osu"),
}
};
for (int i = 0; i < 8; i++)
beatmapSet.Files.Add(new RealmNamedFileUsage(createRealmFile(), $"hitsound{i}.mp3"));
foreach (var b in beatmapSet.Beatmaps)
b.BeatmapSet = beatmapSet;
return beatmapSet;
}
protected static RealmRuleset CreateRuleset() =>
new RealmRuleset(0, "osu!", "osu", true);
private class RealmTestGame : Framework.Game
{
public RealmTestGame(Func<Task> work)
{
// ReSharper disable once AsyncVoidLambda
Scheduler.Add(async () =>
{
await work().ConfigureAwait(true);
Exit();
});
}
public RealmTestGame(Action work)
{
Scheduler.Add(() =>
{
work();
Exit();
});
}
}
private static long getFileSize(Storage testStorage, RealmContextFactory realmFactory)
{
try
{
using (var stream = testStorage.GetStream(realmFactory.Filename))
return stream?.Length ?? 0;
}
catch
{
// windows runs may error due to file still being open.
return 0;
}
}
}
}

View File

@ -32,7 +32,7 @@ namespace osu.Game.Tests.Database
storage = new NativeStorage(directory.FullName); storage = new NativeStorage(directory.FullName);
realmContextFactory = new RealmContextFactory(storage); realmContextFactory = new RealmContextFactory(storage, "test");
keyBindingStore = new RealmKeyBindingStore(realmContextFactory); keyBindingStore = new RealmKeyBindingStore(realmContextFactory);
} }
@ -53,9 +53,9 @@ namespace osu.Game.Tests.Database
private int queryCount(GlobalAction? match = null) private int queryCount(GlobalAction? match = null)
{ {
using (var usage = realmContextFactory.GetForRead()) using (var realm = realmContextFactory.CreateContext())
{ {
var results = usage.Realm.All<RealmKeyBinding>(); var results = realm.All<RealmKeyBinding>();
if (match.HasValue) if (match.HasValue)
results = results.Where(k => k.ActionInt == (int)match.Value); results = results.Where(k => k.ActionInt == (int)match.Value);
return results.Count(); return results.Count();
@ -69,26 +69,24 @@ namespace osu.Game.Tests.Database
keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>()); keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>());
using (var primaryUsage = realmContextFactory.GetForRead()) using (var primaryRealm = realmContextFactory.CreateContext())
{ {
var backBinding = primaryUsage.Realm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back); var backBinding = primaryRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape })); Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
var tsr = ThreadSafeReference.Create(backBinding); var tsr = ThreadSafeReference.Create(backBinding);
using (var usage = realmContextFactory.GetForWrite()) using (var threadedContext = realmContextFactory.CreateContext())
{ {
var binding = usage.Realm.ResolveReference(tsr); var binding = threadedContext.ResolveReference(tsr);
binding.KeyCombination = new KeyCombination(InputKey.BackSpace); threadedContext.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace));
usage.Commit();
} }
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
// check still correct after re-query. // check still correct after re-query.
backBinding = primaryUsage.Realm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back); backBinding = primaryRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
} }
} }

View File

@ -53,7 +53,7 @@ namespace osu.Game.Tests.Editing
BeatDivisor.Value = 1; BeatDivisor.Value = 1;
composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1; composer.EditorBeatmap.Difficulty.SliderMultiplier = 1;
composer.EditorBeatmap.ControlPointInfo.Clear(); composer.EditorBeatmap.ControlPointInfo.Clear();
composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 1 }); composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 1 });
@ -64,7 +64,7 @@ namespace osu.Game.Tests.Editing
[TestCase(2)] [TestCase(2)]
public void TestSliderMultiplier(float multiplier) public void TestSliderMultiplier(float multiplier)
{ {
AddStep($"set multiplier = {multiplier}", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = multiplier); AddStep($"set multiplier = {multiplier}", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = multiplier);
assertSnapDistance(100 * multiplier); assertSnapDistance(100 * multiplier);
} }
@ -97,7 +97,7 @@ namespace osu.Game.Tests.Editing
assertDurationToDistance(500, 50); assertDurationToDistance(500, 50);
assertDurationToDistance(1000, 100); assertDurationToDistance(1000, 100);
AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2); AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2);
assertDurationToDistance(500, 100); assertDurationToDistance(500, 100);
assertDurationToDistance(1000, 200); assertDurationToDistance(1000, 200);
@ -118,7 +118,7 @@ namespace osu.Game.Tests.Editing
assertDistanceToDuration(50, 500); assertDistanceToDuration(50, 500);
assertDistanceToDuration(100, 1000); assertDistanceToDuration(100, 1000);
AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2); AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2);
assertDistanceToDuration(100, 500); assertDistanceToDuration(100, 500);
assertDistanceToDuration(200, 1000); assertDistanceToDuration(200, 1000);
@ -143,7 +143,7 @@ namespace osu.Game.Tests.Editing
assertSnappedDuration(200, 2000); assertSnappedDuration(200, 2000);
assertSnappedDuration(250, 3000); assertSnappedDuration(250, 3000);
AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2); AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2);
assertSnappedDuration(0, 0); assertSnappedDuration(0, 0);
assertSnappedDuration(50, 0); assertSnappedDuration(50, 0);
@ -175,7 +175,7 @@ namespace osu.Game.Tests.Editing
assertSnappedDistance(200, 200); assertSnappedDistance(200, 200);
assertSnappedDistance(250, 200); assertSnappedDistance(250, 200);
AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2); AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2);
assertSnappedDistance(50, 0); assertSnappedDistance(50, 0);
assertSnappedDistance(100, 0); assertSnappedDistance(100, 0);

View File

@ -165,7 +165,7 @@ namespace osu.Game.Tests.Gameplay
{ {
var beatmap = new Beatmap var beatmap = new Beatmap
{ {
BeatmapInfo = { BaseDifficulty = { DrainRate = 10 } }, Difficulty = { DrainRate = 10 }
}; };
beatmap.HitObjects.Add(new JudgeableHitObject { StartTime = 0 }); beatmap.HitObjects.Add(new JudgeableHitObject { StartTime = 0 });
@ -200,7 +200,7 @@ namespace osu.Game.Tests.Gameplay
{ {
var beatmap = new Beatmap var beatmap = new Beatmap
{ {
BeatmapInfo = { BaseDifficulty = { DrainRate = 10 } }, Difficulty = { DrainRate = 10 }
}; };
for (double time = startTime; time <= endTime; time += 100) for (double time = startTime; time <= endTime; time += 100)

View File

@ -17,7 +17,7 @@ namespace osu.Game.Tests
protected virtual TestOsuGameBase LoadOsuIntoHost(GameHost host, bool withBeatmap = false) protected virtual TestOsuGameBase LoadOsuIntoHost(GameHost host, bool withBeatmap = false)
{ {
var osu = new TestOsuGameBase(withBeatmap); var osu = new TestOsuGameBase(withBeatmap);
Task.Run(() => host.Run(osu)) Task.Factory.StartNew(() => host.Run(osu), TaskCreationOptions.LongRunning)
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted); .ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");

View File

@ -19,7 +19,7 @@ namespace osu.Game.Tests.Localisation
Title = "Romanised title", Title = "Romanised title",
TitleUnicode = "Unicode Title" TitleUnicode = "Unicode Title"
}; };
var romanisableString = metadata.ToRomanisableString(); var romanisableString = metadata.GetDisplayTitleRomanisable();
Assert.AreEqual(metadata.ToString(), romanisableString.Romanised); Assert.AreEqual(metadata.ToString(), romanisableString.Romanised);
Assert.AreEqual($"{metadata.ArtistUnicode} - {metadata.TitleUnicode}", romanisableString.Original); Assert.AreEqual($"{metadata.ArtistUnicode} - {metadata.TitleUnicode}", romanisableString.Original);
@ -33,7 +33,7 @@ namespace osu.Game.Tests.Localisation
Artist = "Romanised Artist", Artist = "Romanised Artist",
Title = "Romanised title" Title = "Romanised title"
}; };
var romanisableString = metadata.ToRomanisableString(); var romanisableString = metadata.GetDisplayTitleRomanisable();
Assert.AreEqual(romanisableString.Romanised, romanisableString.Original); Assert.AreEqual(romanisableString.Romanised, romanisableString.Original);
} }

View File

@ -239,7 +239,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
match = shouldMatch; match = shouldMatch;
} }
public bool Matches(BeatmapInfo beatmap) => match; public bool Matches(BeatmapInfo beatmapInfo) => match;
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) => false; public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) => false;
} }
} }

View File

@ -256,7 +256,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
{ {
public string CustomValue { get; set; } public string CustomValue { get; set; }
public bool Matches(BeatmapInfo beatmap) => true; public bool Matches(BeatmapInfo beatmapInfo) => true;
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
{ {

View File

@ -42,7 +42,7 @@ namespace osu.Game.Tests.Online
AddAssert("response event fired", () => response != null); AddAssert("response event fired", () => response != null);
AddAssert("request has response", () => request.Result == response); AddAssert("request has response", () => request.Response == response);
} }
[Test] [Test]

View File

@ -156,20 +156,49 @@ namespace osu.Game.Tests.Online
{ {
public TaskCompletionSource<bool> AllowImport = new TaskCompletionSource<bool>(); public TaskCompletionSource<bool> AllowImport = new TaskCompletionSource<bool>();
public Task<BeatmapSetInfo> CurrentImportTask { get; private set; } public Task<ILive<BeatmapSetInfo>> CurrentImportTask { get; private set; }
protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null)
=> new TestDownloadRequest(set); : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap)
public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false)
: base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap, performOnlineLookups)
{ {
} }
public override async Task<BeatmapSetInfo> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host)
{ {
await AllowImport.Task.ConfigureAwait(false); return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, api, host);
return await (CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)).ConfigureAwait(false); }
protected override BeatmapModelDownloader CreateBeatmapModelDownloader(IBeatmapModelManager manager, IAPIProvider api, GameHost host)
{
return new TestBeatmapModelDownloader(manager, api, host);
}
internal class TestBeatmapModelDownloader : BeatmapModelDownloader
{
public TestBeatmapModelDownloader(IBeatmapModelManager modelManager, IAPIProvider apiProvider, GameHost gameHost)
: base(modelManager, apiProvider, gameHost)
{
}
protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize)
=> new TestDownloadRequest(set);
}
internal class TestBeatmapModelManager : BeatmapModelManager
{
private readonly TestBeatmapManager testBeatmapManager;
public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost)
: base(storage, databaseContextFactory, rulesetStore, gameHost)
{
this.testBeatmapManager = testBeatmapManager;
}
public override async Task<ILive<BeatmapSetInfo>> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
{
await testBeatmapManager.AllowImport.Task.ConfigureAwait(false);
return await (testBeatmapManager.CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)).ConfigureAwait(false);
}
} }
} }

View File

@ -0,0 +1,15 @@
osu file format v14
[Events]
Sprite,Background,TopCentre,"zero-times.png",320,240
L,1000,0
F,0,0,1000,0,1
F,0,1000,2000,1,0
Sprite,Background,TopCentre,"one-time.png",320,240
L,4000,1
F,0,0,1000,0,1
F,0,1000,2000,1,0
Sprite,Background,TopCentre,"many-times.png",320,240
L,9000,40
F,0,0,1000,0,1
F,0,1000,2000,1,0

View File

@ -141,7 +141,7 @@ namespace osu.Game.Tests.Scores.IO
var beatmapManager = osu.Dependencies.Get<BeatmapManager>(); var beatmapManager = osu.Dependencies.Get<BeatmapManager>();
var scoreManager = osu.Dependencies.Get<ScoreManager>(); var scoreManager = osu.Dependencies.Get<ScoreManager>();
beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.Beatmap.ID))); beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.BeatmapInfo.ID)));
Assert.That(scoreManager.Query(s => s.ID == imported.ID).DeletePending, Is.EqualTo(true)); Assert.That(scoreManager.Query(s => s.ID == imported.ID).DeletePending, Is.EqualTo(true));
var secondImport = await LoadScoreIntoOsu(osu, imported); var secondImport = await LoadScoreIntoOsu(osu, imported);
@ -181,7 +181,7 @@ namespace osu.Game.Tests.Scores.IO
{ {
var beatmapManager = osu.Dependencies.Get<BeatmapManager>(); var beatmapManager = osu.Dependencies.Get<BeatmapManager>();
score.Beatmap ??= beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); score.BeatmapInfo ??= beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
score.Ruleset ??= new OsuRuleset().RulesetInfo; score.Ruleset ??= new OsuRuleset().RulesetInfo;
var scoreManager = osu.Dependencies.Get<ScoreManager>(); var scoreManager = osu.Dependencies.Get<ScoreManager>();

View File

@ -196,7 +196,7 @@ namespace osu.Game.Tests.Skins.IO
private async Task<SkinInfo> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null) private async Task<SkinInfo> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null)
{ {
var skinManager = osu.Dependencies.Get<SkinManager>(); var skinManager = osu.Dependencies.Get<SkinManager>();
return await skinManager.Import(archive); return (await skinManager.Import(archive)).Value;
} }
} }
} }

View File

@ -25,7 +25,7 @@ namespace osu.Game.Tests.Skins
private void load() private void load()
{ {
var imported = beatmaps.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-beatmap.osz"))).Result; var imported = beatmaps.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-beatmap.osz"))).Result;
beatmap = beatmaps.GetWorkingBeatmap(imported.Beatmaps[0]); beatmap = beatmaps.GetWorkingBeatmap(imported.Value.Beatmaps[0]);
beatmap.LoadTrack(); beatmap.LoadTrack();
} }

View File

@ -6,7 +6,6 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
@ -65,10 +64,9 @@ namespace osu.Game.Tests.Skins
public new void TriggerSourceChanged() => base.TriggerSourceChanged(); public new void TriggerSourceChanged() => base.TriggerSourceChanged();
protected override void OnSourceChanged() protected override void RefreshSources()
{ {
ResetSources(); SetSources(sources);
sources.ForEach(AddSource);
} }
} }

View File

@ -24,7 +24,7 @@ namespace osu.Game.Tests.Skins
private void load() private void load()
{ {
var imported = skins.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-skin.osk"))).Result; var imported = skins.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-skin.osk"))).Result;
skin = skins.GetSkin(imported); skin = skins.GetSkin(imported.Value);
} }
[Test] [Test]

View File

@ -0,0 +1,167 @@
// 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 ManagedBass.Fx;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Audio.Effects;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Tests.Visual.Audio
{
public class TestSceneAudioFilter : OsuTestScene
{
private OsuSpriteText lowPassText;
private AudioFilter lowPassFilter;
private OsuSpriteText highPassText;
private AudioFilter highPassFilter;
private Track track;
private WaveformTestBeatmap beatmap;
private OsuSliderBar<int> lowPassSlider;
private OsuSliderBar<int> highPassSlider;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
beatmap = new WaveformTestBeatmap(audio);
track = beatmap.LoadTrack();
Add(new FillFlowContainer
{
Children = new Drawable[]
{
lowPassFilter = new AudioFilter(audio.TrackMixer),
highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
lowPassText = new OsuSpriteText
{
Padding = new MarginPadding(20),
Text = $"Low Pass: {lowPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40)
},
lowPassSlider = new OsuSliderBar<int>
{
Width = 500,
Height = 50,
Padding = new MarginPadding(20),
Current = new BindableInt
{
MinValue = 0,
MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
}
},
highPassText = new OsuSpriteText
{
Padding = new MarginPadding(20),
Text = $"High Pass: {highPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40)
},
highPassSlider = new OsuSliderBar<int>
{
Width = 500,
Height = 50,
Padding = new MarginPadding(20),
Current = new BindableInt
{
MinValue = 0,
MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
}
}
}
});
lowPassSlider.Current.ValueChanged += e =>
{
lowPassText.Text = $"Low Pass: {e.NewValue}hz";
lowPassFilter.Cutoff = e.NewValue;
};
highPassSlider.Current.ValueChanged += e =>
{
highPassText.Text = $"High Pass: {e.NewValue}hz";
highPassFilter.Cutoff = e.NewValue;
};
}
#region Overrides of Drawable
protected override void Update()
{
base.Update();
highPassSlider.Current.Value = highPassFilter.Cutoff;
lowPassSlider.Current.Value = lowPassFilter.Cutoff;
}
#endregion
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Play Track", () => track.Start());
AddStep("Reset filters", () =>
{
lowPassFilter.Cutoff = AudioFilter.MAX_LOWPASS_CUTOFF;
highPassFilter.Cutoff = 0;
});
waitTrackPlay();
}
[Test]
public void TestLowPassSweep()
{
AddStep("Filter Sweep", () =>
{
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic);
});
waitTrackPlay();
AddStep("Filter Sweep (reverse)", () =>
{
lowPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
});
waitTrackPlay();
AddStep("Stop track", () => track.Stop());
}
[Test]
public void TestHighPassSweep()
{
AddStep("Filter Sweep", () =>
{
highPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
});
waitTrackPlay();
AddStep("Filter Sweep (reverse)", () =>
{
highPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic);
});
waitTrackPlay();
AddStep("Stop track", () => track.Stop());
}
private void waitTrackPlay() => AddWaitStep("Let track play", 10);
}
}

View File

@ -232,7 +232,7 @@ namespace osu.Game.Tests.Visual.Background
AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(new ScoreInfo AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(new ScoreInfo
{ {
User = new User { Username = "osu!" }, User = new User { Username = "osu!" },
Beatmap = new TestBeatmap(Ruleset.Value).BeatmapInfo, BeatmapInfo = new TestBeatmap(Ruleset.Value).BeatmapInfo,
Ruleset = Ruleset.Value, Ruleset = Ruleset.Value,
}))); })));
@ -286,7 +286,7 @@ namespace osu.Game.Tests.Visual.Background
private void setupUserSettings() private void setupUserSettings()
{ {
AddUntilStep("Song select is current", () => songSelect.IsCurrentScreen()); AddUntilStep("Song select is current", () => songSelect.IsCurrentScreen());
AddUntilStep("Song select has selection", () => songSelect.Carousel?.SelectedBeatmap != null); AddUntilStep("Song select has selection", () => songSelect.Carousel?.SelectedBeatmapInfo != null);
AddStep("Set default user settings", () => AddStep("Set default user settings", () =>
{ {
SelectedMods.Value = SelectedMods.Value.Concat(new[] { new OsuModNoFail() }).ToArray(); SelectedMods.Value = SelectedMods.Value.Concat(new[] { new OsuModNoFail() }).ToArray();

View File

@ -0,0 +1,132 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneBlueprintOrdering : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
private EditorBlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<EditorBlueprintContainer>().First();
[Test]
public void TestSelectedObjectHasPriorityWhenOverlapping()
{
var firstSlider = new Slider
{
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2()),
new PathControlPoint(new Vector2(150, -50)),
new PathControlPoint(new Vector2(300, 0))
}),
Position = new Vector2(0, 100)
};
var secondSlider = new Slider
{
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2()),
new PathControlPoint(new Vector2(-50, 50)),
new PathControlPoint(new Vector2(-100, 100))
}),
Position = new Vector2(200, 0)
};
AddStep("add overlapping sliders", () =>
{
EditorBeatmap.Add(firstSlider);
EditorBeatmap.Add(secondSlider);
});
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(firstSlider));
AddStep("move mouse to common point", () =>
{
var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece>().ElementAt(1).ScreenSpaceDrawQuad.Centre;
InputManager.MoveMouseTo(pos);
});
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddAssert("selection is unchanged", () => EditorBeatmap.SelectedHitObjects.Single() == firstSlider);
}
[Test]
public void TestOverlappingObjectsWithSameStartTime()
{
AddStep("add overlapping circles", () =>
{
EditorBeatmap.Add(createHitCircle(50, OsuPlayfield.BASE_SIZE / 2));
EditorBeatmap.Add(createHitCircle(50, OsuPlayfield.BASE_SIZE / 2 + new Vector2(-10, -20)));
EditorBeatmap.Add(createHitCircle(50, OsuPlayfield.BASE_SIZE / 2 + new Vector2(10, -20)));
});
AddStep("click at centre of playfield", () =>
{
var hitObjectContainer = Editor.ChildrenOfType<HitObjectContainer>().Single();
var centre = hitObjectContainer.ToScreenSpace(OsuPlayfield.BASE_SIZE / 2);
InputManager.MoveMouseTo(centre);
InputManager.Click(MouseButton.Left);
});
AddAssert("frontmost object selected", () =>
{
var hasCombo = Editor.ChildrenOfType<HitCircleSelectionBlueprint>().Single(b => b.IsSelected).Item as IHasComboInformation;
return hasCombo?.IndexInCurrentCombo == 0;
});
}
[Test]
public void TestPlacementOfConcurrentObjectWithDuration()
{
AddStep("seek to timing point", () => EditorClock.Seek(2170));
AddStep("add hit circle", () => EditorBeatmap.Add(createHitCircle(2170, Vector2.Zero)));
AddStep("choose spinner placement tool", () =>
{
InputManager.Key(Key.Number4);
var hitObjectContainer = Editor.ChildrenOfType<HitObjectContainer>().Single();
InputManager.MoveMouseTo(hitObjectContainer.ScreenSpaceDrawQuad.Centre);
});
AddStep("begin placing spinner", () =>
{
InputManager.Click(MouseButton.Left);
});
AddStep("end placing spinner", () =>
{
EditorClock.Seek(2500);
InputManager.Click(MouseButton.Right);
});
AddAssert("two timeline blueprints present", () => Editor.ChildrenOfType<TimelineHitObjectBlueprint>().Count() == 2);
}
private HitCircle createHitCircle(double startTime, Vector2 position) => new HitCircle
{
StartTime = startTime,
Position = position,
};
}
}

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