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

Merge branch 'master' into fix-invisible-sinners

This commit is contained in:
smoogipoo 2021-10-15 20:45:40 +09:00
commit 1a18d3598a
232 changed files with 4910 additions and 1460 deletions

1
.github/FUNDING.yml vendored
View File

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

View File

@ -79,9 +79,14 @@ jobs:
run: |
# 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
dotnet codefilesanity | while read -r line; do
echo "::warning::$line"
done
exit_code=0
while read -r line; do
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.
# - name: .NET Format (Dry Run)

View File

@ -30,3 +30,5 @@ jobs:
name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
path: "*.trx"
reporter: dotnet-trx
list-suites: 'failed'
list-tests: 'failed'

View File

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

View File

@ -14,11 +14,11 @@ namespace osu.Game.Rulesets.Catch.Tests
{
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)
=> base.Test(expected, name);
[TestCase(5.169743871843191d, "diffcalc-test")]
[TestCase(5.1696411260785498d, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new CatchModDoubleTime());

View File

@ -42,9 +42,8 @@ namespace osu.Game.Rulesets.Catch.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate;

View File

@ -13,6 +13,7 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
@ -101,27 +102,27 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
throw new System.NotImplementedException();
}
public override float GetBeatSnapDistanceAt(double referenceTime)
public override float GetBeatSnapDistanceAt(HitObject referenceObject)
{
throw new System.NotImplementedException();
}
public override float DurationToDistance(double referenceTime, double duration)
public override float DurationToDistance(HitObject referenceObject, double duration)
{
throw new System.NotImplementedException();
}
public override double DistanceToDuration(double referenceTime, float distance)
public override double DistanceToDuration(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}
public override double GetSnappedDurationFromDistance(double referenceTime, float distance)
public override double GetSnappedDurationFromDistance(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}
public override float GetSnappedDistanceFromDistance(double referenceTime, float distance)
public override float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}

View File

@ -388,7 +388,7 @@ namespace osu.Game.Rulesets.Mania.Tests
},
};
beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
}
AddStep("load player", () =>

View File

@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Mania.Tests
},
});
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });

View File

@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
Debug.Assert(distanceData != null);
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
DifficultyControlPoint difficultyPoint = hitObject.DifficultyControlPoint;
double beatLength;
#pragma warning disable 618
@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
#pragma warning restore 618
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
else
beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity;
SpanCount = repeatsData?.SpanCount() ?? 1;
StartTime = (int)Math.Round(hitObject.StartTime);

View File

@ -47,8 +47,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
Mods = mods,
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate),
GreatHitWindow = Math.Ceiling(getHitWindow300(mods) / clockRate),
ScoreMultiplier = getScoreMultiplier(mods),
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
Skills = skills

View File

@ -13,6 +13,7 @@ SliderTickRate:1
[TimingPoints]
0,500,4,1,0,100,1,0
10000,-150,4,1,0,100,1,0
[HitObjects]
51,192,500,128,0,1500:1:0:0:0:

View File

@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Mania.UI
// For non-mania beatmap, speed changes should only happen through timing points
if (!isForCurrentRuleset)
p.DifficultyPoint = new DifficultyControlPoint();
p.EffectPoint = new EffectControlPoint();
}
BarLines.ForEach(Playfield.Add);

View File

@ -11,6 +11,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects;
@ -179,15 +180,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length;
public float GetBeatSnapDistanceAt(HitObject referenceObject) => (float)beat_length;
public float DurationToDistance(double referenceTime, double duration) => (float)duration;
public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration;
public double DistanceToDuration(double referenceTime, float distance) => distance;
public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0;
public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0;
public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0;
}
}
}

View File

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

View File

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

View File

@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
config.SetValue(OsuSetting.AutoCursorSize, true);
gameplayState.Beatmap.Difficulty.CircleSize = val;
Scheduler.AddOnce(() => loadContent(false));
Scheduler.AddOnce(loadContent);
});
AddStep("test cursor container", () => loadContent(false));
@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.Difficulty.CircleSize = circleSize);
AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true));
AddStep("load content", () => loadContent());
AddStep("load content", loadContent);
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale);
@ -98,7 +98,9 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("load content", () => loadContent(false, () => new SkinProvidingContainer(new TopLeftCursorSkin())));
}
private void loadContent(bool automated = true, Func<SkinProvidingContainer> skinProvider = null)
private void loadContent() => loadContent(false);
private void loadContent(bool automated, Func<SkinProvidingContainer> skinProvider = null)
{
SetContents(_ =>
{

View File

@ -407,8 +407,6 @@ namespace osu.Game.Rulesets.Osu.Tests
},
});
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
SelectedMods.Value = new[] { new OsuModClassic() };
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
@ -439,6 +437,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public TestSlider()
{
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
DefaultsApplied += _ =>
{
HeadCircle.HitWindows = new TestHitWindows();

View File

@ -13,6 +13,7 @@ using osuTK.Graphics;
using osu.Game.Rulesets.Mods;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
@ -328,10 +329,14 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier)
{
var cpi = new ControlPointInfo();
cpi.Add(0, new DifficultyControlPoint { SpeedMultiplier = speedMultiplier });
var cpi = new LegacyControlPointInfo();
cpi.Add(0, new DifficultyControlPoint { SliderVelocity = speedMultiplier });
slider.ApplyDefaults(cpi, new BeatmapDifficulty { CircleSize = circleSize, SliderTickRate = 3 });
slider.ApplyDefaults(cpi, new BeatmapDifficulty
{
CircleSize = circleSize,
SliderTickRate = 3
});
var drawable = CreateDrawableSlider(slider);

View File

@ -348,6 +348,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider_start,
Position = new Vector2(0, 0),
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f },
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
@ -362,8 +363,6 @@ namespace osu.Game.Rulesets.Osu.Tests
},
});
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>

View File

@ -30,6 +30,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneSpinnerRotation : TestSceneOsuPlayer
{
private const double spinner_start_time = 100;
private const double spinner_duration = 6000;
[Resolved]
private AudioManager audioManager { get; set; }
@ -77,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests
double finalTrackerRotation = 0, trackerRotationTolerance = 0;
double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
addSeekStep(5000);
addSeekStep(spinner_start_time + 5000);
AddStep("retrieve disc rotation", () =>
{
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests
});
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation);
addSeekStep(2500);
addSeekStep(spinner_start_time + 2500);
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.
// 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.
() => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100));
addSeekStep(5000);
addSeekStep(spinner_start_time + 5000);
AddAssert("is disc rotation almost same",
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance));
AddAssert("is symbol rotation almost same",
@ -140,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestSpinnerNormalBonusRewinding()
{
addSeekStep(1000);
addSeekStep(spinner_start_time + 1000);
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));
}
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)
{
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
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
{
Position = new Vector2(256, 192),
EndTime = 6000,
StartTime = spinner_start_time,
Duration = spinner_duration
},
}
};

View File

@ -369,8 +369,6 @@ namespace osu.Game.Rulesets.Osu.Tests
},
});
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
@ -399,6 +397,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public TestSlider()
{
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
DefaultsApplied += _ =>
{
HeadCircle.HitWindows = new TestHitWindows();

View File

@ -11,6 +11,7 @@ using System.Linq;
using System.Threading;
using osu.Game.Rulesets.Osu.UI;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Beatmaps.Legacy;
namespace osu.Game.Rulesets.Osu.Beatmaps
{
@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / beatmap.ControlPointInfo.DifficultyPointAt(original.StartTime).SpeedMultiplier : 1
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1
}.Yield();
case IHasDuration endTimeData:

View File

@ -56,7 +56,7 @@ 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 preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate;
int maxCombo = beatmap.HitObjects.Count;
@ -102,8 +102,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
HitWindows hitWindows = new OsuHitWindows();
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 = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate;
hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
return new Skill[]
{

View File

@ -40,9 +40,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
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.
// Custom multipliers for NoFail and SpunOut.
if (mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss);
@ -114,11 +114,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
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))
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 *= approachRateBonus;
@ -155,14 +157,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
// Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.
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);
}
// 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);
// Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
@ -199,6 +207,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
accuracyValue *= 1.14;
else if (mods.Any(m => m is OsuModHidden))
accuracyValue *= 1.08;
if (mods.Any(m => m is OsuModFlashlight))
accuracyValue *= 1.02;

View File

@ -22,17 +22,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
}
protected override double SkillMultiplier => 26.25;
protected override double StrainDecayBase => 0.15;
private double currentStrain = 1;
protected override double StrainValueOf(DifficultyHitObject current)
private double skillMultiplier => 26.25;
private double strainDecayBase => 0.15;
private double strainValueOf(DifficultyHitObject current)
{
if (current.BaseObject is Spinner)
return 0;
var osuCurrent = (OsuDifficultyHitObject)current;
double result = 0;
double aimStrain = 0;
if (Previous.Count > 0)
{
@ -46,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
Math.Max(osuPrevious.JumpDistance - scale, 0)
* Math.Pow(Math.Sin(osuCurrent.Angle.Value - angle_bonus_begin), 2)
* Math.Max(osuCurrent.JumpDistance - scale, 0));
result = 1.4 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime);
aimStrain = 1.4 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime);
}
}
@ -54,11 +56,23 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
double travelDistanceExp = applyDiminishingExp(osuCurrent.TravelDistance);
return Math.Max(
result + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(osuCurrent.StrainTime, timing_threshold),
aimStrain + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(osuCurrent.StrainTime, timing_threshold),
(Math.Sqrt(travelDistanceExp * jumpDistanceExp) + jumpDistanceExp + travelDistanceExp) / osuCurrent.StrainTime
);
}
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)
{
currentStrain *= strainDecay(current.DeltaTime);
currentStrain += strainValueOf(current) * skillMultiplier;
return currentStrain;
}
}
}

View File

@ -19,12 +19,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
}
protected override double SkillMultiplier => 0.15;
protected override double StrainDecayBase => 0.15;
private double skillMultiplier => 0.15;
private double strainDecayBase => 0.15;
protected override double DecayWeight => 1.0;
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)
return 0;
@ -62,5 +63,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
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
{
public abstract class OsuStrainSkill : StrainDecaySkill
public abstract class OsuStrainSkill : StrainSkill
{
/// <summary>
/// The number of sections with the highest strains, which the peak strain reductions will apply to.

View File

@ -16,19 +16,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
public class Speed : OsuStrainSkill
{
private const double single_spacing_threshold = 125;
private const double angle_bonus_begin = 5 * Math.PI / 6;
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 rhythm_multiplier = 0.75;
private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max.
private const double min_speed_bonus = 75; // ~200BPM
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;
public Speed(Mod[] mods, double hitWindowGreat)
@ -37,52 +39,138 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
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)
return 0;
var osuCurrent = (OsuDifficultyHitObject)current;
var osuPrevious = Previous.Count > 0 ? (OsuDifficultyHitObject)Previous[0] : null;
int previousIslandSize = 0;
double distance = Math.Min(single_spacing_threshold, osuCurrent.TravelDistance + osuCurrent.JumpDistance);
double strainTime = osuCurrent.StrainTime;
double rhythmComplexitySum = 0;
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 speedWindowRatio = strainTime / greatWindowFull;
// Aim to nerf cheesy rhythms (Very fast consecutive doubles with large deltatimes between)
if (osuPrevious != null && strainTime < greatWindowFull && osuPrevious.StrainTime > strainTime)
strainTime = Interpolation.Lerp(osuPrevious.StrainTime, strainTime, speedWindowRatio);
if (osuPrevObj != null && strainTime < greatWindowFull && osuPrevObj.StrainTime > strainTime)
strainTime = Interpolation.Lerp(osuPrevObj.StrainTime, strainTime, speedWindowRatio);
// 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.
strainTime /= Math.Clamp((strainTime / greatWindowFull) / 0.93, 0.92, 1);
// derive speedBonus for calculation
double speedBonus = 1.0;
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)
{
angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - osuCurrent.Angle.Value)), 2) / 3.57;
return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime;
}
if (osuCurrent.Angle.Value < pi_over_2)
{
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);
}
}
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
return (1 + (speedBonus - 1) * 0.75)
* angleBonus
* (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5))
/ strainTime;
protected override double CalculateInitialStrain(double time) => (currentStrain * currentRhythm) * strainDecay(time - Previous[0].StartTime);
protected override double StrainValueAt(DifficultyHitObject current)
{
currentStrain *= strainDecay(current.DeltaTime);
currentStrain += strainValueOf(current) * skillMultiplier;
currentRhythm = calculateRhythmBonus(current);
return currentStrain * currentRhythm;
}
}
}

View File

@ -8,12 +8,14 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Screens.Edit;
using osuTK;
using osuTK.Input;
@ -67,6 +69,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
inputManager = GetContainingInputManager();
}
[Resolved]
private EditorBeatmap editorBeatmap { get; set; }
public override void UpdateTimeAndPosition(SnapResult result)
{
base.UpdateTimeAndPosition(result);
@ -75,6 +80,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
case SliderPlacementState.Initial:
BeginPlacement();
var nearestDifficultyPoint = editorBeatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < HitObject.StartTime)?.DifficultyControlPoint?.DeepClone() as DifficultyControlPoint;
HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint();
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
break;
@ -212,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updateSlider()
{
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
bodyPiece.UpdateFrom(HitObject);
headCirclePiece.UpdateFrom(HitObject.HeadCircle);

View File

@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updatePath()
{
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
editorBeatmap?.Update(HitObject);
}

View File

@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public class OsuDistanceSnapGrid : CircularDistanceSnapGrid
{
public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null)
: base(hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime)
: base(hitObject, hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime)
{
Masking = true;
}

View File

@ -0,0 +1,76 @@
// 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 osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Mods;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModNoScope : Mod, IUpdatableByPlayfield, IApplicableToScoreProcessor
{
/// <summary>
/// Slightly higher than the cutoff for <see cref="Drawable.IsPresent"/>.
/// </summary>
private const float min_alpha = 0.0002f;
private const float transition_duration = 100;
public override string Name => "No Scope";
public override string Acronym => "NS";
public override ModType Type => ModType.Fun;
public override IconUsage? Icon => FontAwesome.Solid.EyeSlash;
public override string Description => "Where's the cursor?";
public override double ScoreMultiplier => 1;
private BindableNumber<int> currentCombo;
private float targetAlpha;
[SettingSource(
"Hidden at combo",
"The combo count at which the cursor becomes completely hidden",
SettingControlType = typeof(SettingsSlider<int, HiddenComboSlider>)
)]
public BindableInt HiddenComboCount { get; } = new BindableInt
{
Default = 10,
Value = 10,
MinValue = 0,
MaxValue = 50,
};
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
{
if (HiddenComboCount.Value == 0) return;
currentCombo = scoreProcessor.Combo.GetBoundCopy();
currentCombo.BindValueChanged(combo =>
{
targetAlpha = Math.Max(min_alpha, 1 - (float)combo.NewValue / HiddenComboCount.Value);
}, true);
}
public virtual void Update(Playfield playfield)
{
playfield.Cursor.Alpha = (float)Interpolation.Lerp(playfield.Cursor.Alpha, targetAlpha, Math.Clamp(playfield.Time.Elapsed / transition_duration, 0, 1));
}
}
public class HiddenComboSlider : OsuSliderBar<int>
{
public override LocalisableString TooltipText => Current.Value == 0 ? "always hidden" : base.TooltipText;
}
}

View File

@ -140,9 +140,8 @@ namespace osu.Game.Rulesets.Osu.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
@ -175,7 +174,6 @@ namespace osu.Game.Rulesets.Osu.Objects
StartTime = e.Time,
Position = Position,
StackHeight = StackHeight,
SampleControlPoint = SampleControlPoint,
});
break;

View File

@ -192,6 +192,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModBarrelRoll(),
new OsuModApproachDifferent(),
new OsuModMuted(),
new OsuModNoScope(),
};
case ModType.System:

View File

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

View File

@ -47,10 +47,10 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
protected override Beatmap<TaikoHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{
if (!(original.Difficulty is TaikoMutliplierAppliedDifficulty))
if (!(original.Difficulty is TaikoMultiplierAppliedDifficulty))
{
// Rewrite the beatmap info to add the slider velocity multiplier
original.Difficulty = new TaikoMutliplierAppliedDifficulty(original.Difficulty);
original.Difficulty = new TaikoMultiplierAppliedDifficulty(original.Difficulty);
}
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original, cancellationToken);
@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime);
DifficultyControlPoint difficultyPoint = obj.DifficultyControlPoint;
double beatLength;
#pragma warning disable 618
@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
#pragma warning restore 618
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
else
beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity;
double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate;
@ -191,15 +191,15 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
protected override Beatmap<TaikoHitObject> CreateBeatmap() => new TaikoBeatmap();
private class TaikoMutliplierAppliedDifficulty : BeatmapDifficulty
private class TaikoMultiplierAppliedDifficulty : BeatmapDifficulty
{
public TaikoMutliplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty)
public TaikoMultiplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty)
{
CopyFrom(difficulty);
}
[UsedImplicitly]
public TaikoMutliplierAppliedDifficulty()
public TaikoMultiplierAppliedDifficulty()
{
}
@ -208,14 +208,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
public override void CopyTo(BeatmapDifficulty other)
{
base.CopyTo(other);
if (!(other is TaikoMutliplierAppliedDifficulty))
if (!(other is TaikoMultiplierAppliedDifficulty))
SliderMultiplier /= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
}
public override void CopyFrom(IBeatmapDifficultyInfo other)
{
base.CopyFrom(other);
if (!(other is TaikoMutliplierAppliedDifficulty))
if (!(other is TaikoMultiplierAppliedDifficulty))
SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
}

View File

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

View File

@ -63,9 +63,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
double scoringDistance = base_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
double scoringDistance = base_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
tickSpacing = timingPoint.BeatLength / TickRate;

View File

@ -192,15 +192,15 @@ namespace osu.Game.Tests.Beatmaps.Formats
var difficultyPoint = controlPoints.DifficultyPointAt(0);
Assert.AreEqual(0, difficultyPoint.Time);
Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
Assert.AreEqual(1.0, difficultyPoint.SliderVelocity);
difficultyPoint = controlPoints.DifficultyPointAt(48428);
Assert.AreEqual(0, difficultyPoint.Time);
Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
Assert.AreEqual(1.0, difficultyPoint.SliderVelocity);
difficultyPoint = controlPoints.DifficultyPointAt(116999);
Assert.AreEqual(116999, difficultyPoint.Time);
Assert.AreEqual(0.75, difficultyPoint.SpeedMultiplier, 0.1);
Assert.AreEqual(0.75, difficultyPoint.SliderVelocity, 0.1);
var soundPoint = controlPoints.SamplePointAt(0);
Assert.AreEqual(956, soundPoint.Time);
@ -227,7 +227,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsTrue(effectPoint.KiaiMode);
Assert.IsFalse(effectPoint.OmitFirstBarLine);
effectPoint = controlPoints.EffectPointAt(119637);
effectPoint = controlPoints.EffectPointAt(116637);
Assert.AreEqual(95901, effectPoint.Time);
Assert.IsFalse(effectPoint.KiaiMode);
Assert.IsFalse(effectPoint.OmitFirstBarLine);
@ -249,10 +249,10 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(controlPoints.EffectPoints.Count, Is.EqualTo(3));
Assert.That(controlPoints.SamplePoints.Count, Is.EqualTo(3));
Assert.That(controlPoints.DifficultyPointAt(500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1));
Assert.That(controlPoints.DifficultyPointAt(1500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1));
Assert.That(controlPoints.DifficultyPointAt(2500).SpeedMultiplier, Is.EqualTo(0.75).Within(0.1));
Assert.That(controlPoints.DifficultyPointAt(3500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1));
Assert.That(controlPoints.DifficultyPointAt(500).SliderVelocity, Is.EqualTo(1.5).Within(0.1));
Assert.That(controlPoints.DifficultyPointAt(1500).SliderVelocity, Is.EqualTo(1.5).Within(0.1));
Assert.That(controlPoints.DifficultyPointAt(2500).SliderVelocity, Is.EqualTo(0.75).Within(0.1));
Assert.That(controlPoints.DifficultyPointAt(3500).SliderVelocity, Is.EqualTo(1.5).Within(0.1));
Assert.That(controlPoints.EffectPointAt(500).KiaiMode, Is.True);
Assert.That(controlPoints.EffectPointAt(1500).KiaiMode, Is.True);
@ -279,10 +279,10 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var resStream = TestResources.OpenResource("timingpoint-speedmultiplier-reset.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var controlPoints = decoder.Decode(stream).ControlPointInfo;
var controlPoints = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo;
Assert.That(controlPoints.DifficultyPointAt(0).SpeedMultiplier, Is.EqualTo(0.5).Within(0.1));
Assert.That(controlPoints.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1).Within(0.1));
Assert.That(controlPoints.DifficultyPointAt(0).SliderVelocity, Is.EqualTo(0.5).Within(0.1));
Assert.That(controlPoints.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1).Within(0.1));
}
}
@ -394,12 +394,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var resStream = TestResources.OpenResource("controlpoint-difficulty-multiplier.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var controlPointInfo = decoder.Decode(stream).ControlPointInfo;
var controlPointInfo = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo;
Assert.That(controlPointInfo.DifficultyPointAt(5).SpeedMultiplier, Is.EqualTo(1));
Assert.That(controlPointInfo.DifficultyPointAt(1000).SpeedMultiplier, Is.EqualTo(10));
Assert.That(controlPointInfo.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1.8518518518518519d));
Assert.That(controlPointInfo.DifficultyPointAt(3000).SpeedMultiplier, Is.EqualTo(0.5));
Assert.That(controlPointInfo.DifficultyPointAt(5).SliderVelocity, Is.EqualTo(1));
Assert.That(controlPointInfo.DifficultyPointAt(1000).SliderVelocity, Is.EqualTo(10));
Assert.That(controlPointInfo.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1.8518518518518519d));
Assert.That(controlPointInfo.DifficultyPointAt(3000).SliderVelocity, Is.EqualTo(0.5));
}
}

View File

@ -46,8 +46,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
sort(decoded.beatmap);
sort(decodedAfterEncode.beatmap);
Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize()));
Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
compareBeatmaps(decoded, decodedAfterEncode);
}
[TestCaseSource(nameof(allBeatmaps))]
@ -62,8 +61,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
sort(decoded.beatmap);
sort(decodedAfterEncode.beatmap);
Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize()));
Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
compareBeatmaps(decoded, decodedAfterEncode);
}
[TestCaseSource(nameof(allBeatmaps))]
@ -77,12 +75,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name);
// in this process, we may lose some detail in the control points section.
// let's focus on only the hitobjects.
var originalHitObjects = decoded.beatmap.HitObjects.Serialize();
var newHitObjects = decodedAfterEncode.beatmap.HitObjects.Serialize();
Assert.That(newHitObjects, Is.EqualTo(originalHitObjects));
compareBeatmaps(decoded, decodedAfterEncode);
ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo)
{
@ -97,7 +90,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
// completely ignore "legacy" types, which have been moved to HitObjects.
// even though these would mostly be ignored by the Add call, they will still be available in groups,
// which isn't what we want to be testing here.
if (point is SampleControlPoint)
if (point is SampleControlPoint || point is DifficultyControlPoint)
continue;
newControlPoints.Add(point.Time, point.DeepClone());
@ -107,6 +100,19 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
private void compareBeatmaps((IBeatmap beatmap, TestLegacySkin skin) expected, (IBeatmap beatmap, TestLegacySkin skin) actual)
{
// Check all control points that are still considered to be at a global level.
Assert.That(expected.beatmap.ControlPointInfo.TimingPoints.Serialize(), Is.EqualTo(actual.beatmap.ControlPointInfo.TimingPoints.Serialize()));
Assert.That(expected.beatmap.ControlPointInfo.EffectPoints.Serialize(), Is.EqualTo(actual.beatmap.ControlPointInfo.EffectPoints.Serialize()));
// Check all hitobjects.
Assert.That(expected.beatmap.HitObjects.Serialize(), Is.EqualTo(actual.beatmap.HitObjects.Serialize()));
// Check skin.
Assert.IsTrue(areComboColoursEqual(expected.skin.Configuration, actual.skin.Configuration));
}
[Test]
public void TestEncodeMultiSegmentSliderWithFloatingPointError()
{
@ -156,7 +162,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
private (IBeatmap beatmap, TestLegacySkin beatmapSkin) decodeFromLegacy(Stream stream, string name)
private (IBeatmap beatmap, TestLegacySkin skin) decodeFromLegacy(Stream stream, string name)
{
using (var reader = new LineBufferedReader(stream))
{
@ -174,7 +180,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
private MemoryStream encodeToLegacy((IBeatmap beatmap, ISkin beatmapSkin) fullBeatmap)
private MemoryStream encodeToLegacy((IBeatmap beatmap, ISkin skin) fullBeatmap)
{
var (beatmap, beatmapSkin) = fullBeatmap;
var stream = new MemoryStream();

View File

@ -509,5 +509,17 @@ namespace osu.Game.Tests.Chat
Assert.AreEqual(LinkAction.External, result.Action);
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

@ -1,3 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading;
using System.Threading.Tasks;

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

@ -4,12 +4,13 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Nito.AsyncEx;
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
@ -28,42 +29,109 @@ namespace osu.Game.Tests.Database
protected void RunTestWithRealm(Action<RealmContextFactory, Storage> testAction, [CallerMemberName] string caller = "")
{
AsyncContext.Run(() =>
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller))
{
var testStorage = storage.GetStorageForDirectory(caller);
using (var realmFactory = new RealmContextFactory(testStorage, caller))
host.Run(new RealmTestGame(() =>
{
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
testAction(realmFactory, testStorage);
var testStorage = storage.GetStorageForDirectory(caller);
realmFactory.Dispose();
using (var realmFactory = new RealmContextFactory(testStorage, caller))
{
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
testAction(realmFactory, testStorage);
Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
realmFactory.Compact();
Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}");
}
});
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 = "")
{
AsyncContext.Run(async () =>
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller))
{
var testStorage = storage.GetStorageForDirectory(caller);
using (var realmFactory = new RealmContextFactory(testStorage, caller))
host.Run(new RealmTestGame(async () =>
{
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
await testAction(realmFactory, testStorage);
var testStorage = storage.GetStorageForDirectory(caller);
realmFactory.Dispose();
using (var realmFactory = new RealmContextFactory(testStorage, caller))
{
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
await testAction(realmFactory, testStorage);
Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
realmFactory.Compact();
Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}");
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)

View File

@ -0,0 +1,54 @@
// 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.Game.Models;
using osu.Game.Stores;
namespace osu.Game.Tests.Database
{
public class RulesetStoreTests : RealmTest
{
[Test]
public void TestCreateStore()
{
RunTestWithRealm((realmFactory, storage) =>
{
var rulesets = new RealmRulesetStore(realmFactory, storage);
Assert.AreEqual(4, rulesets.AvailableRulesets.Count());
Assert.AreEqual(4, realmFactory.Context.All<RealmRuleset>().Count());
});
}
[Test]
public void TestCreateStoreTwiceDoesntAddRulesetsAgain()
{
RunTestWithRealm((realmFactory, storage) =>
{
var rulesets = new RealmRulesetStore(realmFactory, storage);
var rulesets2 = new RealmRulesetStore(realmFactory, storage);
Assert.AreEqual(4, rulesets.AvailableRulesets.Count());
Assert.AreEqual(4, rulesets2.AvailableRulesets.Count());
Assert.AreEqual(rulesets.AvailableRulesets.First(), rulesets2.AvailableRulesets.First());
Assert.AreEqual(4, realmFactory.Context.All<RealmRuleset>().Count());
});
}
[Test]
public void TestRetrievedRulesetsAreDetached()
{
RunTestWithRealm((realmFactory, storage) =>
{
var rulesets = new RealmRulesetStore(realmFactory, storage);
Assert.IsTrue((rulesets.AvailableRulesets.First() as RealmRuleset)?.IsManaged == false);
Assert.IsTrue((rulesets.GetRuleset(0) as RealmRuleset)?.IsManaged == false);
Assert.IsTrue((rulesets.GetRuleset("mania") as RealmRuleset)?.IsManaged == false);
});
}
}
}

View File

@ -0,0 +1,104 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Storyboards;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using FileInfo = osu.Game.IO.FileInfo;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckAudioInVideoTest
{
private CheckAudioInVideo check;
private IBeatmap beatmap;
[SetUp]
public void Setup()
{
check = new CheckAudioInVideo();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = new BeatmapSetInfo
{
Files = new List<BeatmapSetFileInfo>(new[]
{
new BeatmapSetFileInfo
{
Filename = "abc123.mp4",
FileInfo = new FileInfo { Hash = "abcdef" }
}
})
}
}
};
}
[Test]
public void TestRegularVideoFile()
{
using (var resourceStream = TestResources.OpenResource("Videos/test-video.mp4"))
Assert.IsEmpty(check.Run(getContext(resourceStream)));
}
[Test]
public void TestVideoFileWithAudio()
{
using (var resourceStream = TestResources.OpenResource("Videos/test-video-with-audio.mp4"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateHasAudioTrack);
}
}
[Test]
public void TestVideoFileWithTrackButNoAudio()
{
using (var resourceStream = TestResources.OpenResource("Videos/test-video-with-track-but-no-audio.mp4"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateHasAudioTrack);
}
}
[Test]
public void TestMissingFile()
{
beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
var issues = check.Run(getContext(null)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateMissingFile);
}
private BeatmapVerifierContext getContext(Stream resourceStream)
{
var storyboard = new Storyboard();
var layer = storyboard.GetLayer("Video");
layer.Add(new StoryboardVideo("abc123.mp4", 0));
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
mockWorkingBeatmap.As<IWorkingBeatmap>().SetupGet(w => w.Storyboard).Returns(storyboard);
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
}
}
}

View File

@ -0,0 +1,128 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ManagedBass;
using Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osuTK.Audio;
using FileInfo = osu.Game.IO.FileInfo;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckTooShortAudioFilesTest
{
private CheckTooShortAudioFiles check;
private IBeatmap beatmap;
[SetUp]
public void Setup()
{
check = new CheckTooShortAudioFiles();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = new BeatmapSetInfo
{
Files = new List<BeatmapSetFileInfo>(new[]
{
new BeatmapSetFileInfo
{
Filename = "abc123.wav",
FileInfo = new FileInfo { Hash = "abcdef" }
}
})
}
}
};
// 0 = No output device. This still allows decoding.
if (!Bass.Init(0) && Bass.LastError != Errors.Already)
throw new AudioException("Could not initialize Bass.");
}
[Test]
public void TestDifferentExtension()
{
beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
beatmap.BeatmapInfo.BeatmapSet.Files.Add(new BeatmapSetFileInfo
{
Filename = "abc123.jpg",
FileInfo = new FileInfo { Hash = "abcdef" }
});
// Should fail to load, but not produce an error due to the extension not being expected to load.
Assert.IsEmpty(check.Run(getContext(null, allowMissing: true)));
}
[Test]
public void TestRegularAudioFile()
{
using (var resourceStream = TestResources.OpenResource("Samples/test-sample.mp3"))
{
Assert.IsEmpty(check.Run(getContext(resourceStream)));
}
}
[Test]
public void TestBlankAudioFile()
{
using (var resourceStream = TestResources.OpenResource("Samples/blank.wav"))
{
// This is a 0 ms duration audio file, commonly used to silence sliderslides/ticks, and so should be fine.
Assert.IsEmpty(check.Run(getContext(resourceStream)));
}
}
[Test]
public void TestTooShortAudioFile()
{
using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateTooShort);
}
}
[Test]
public void TestMissingAudioFile()
{
using (var resourceStream = TestResources.OpenResource("Samples/missing.mp3"))
{
Assert.IsEmpty(check.Run(getContext(resourceStream, allowMissing: true)));
}
}
[Test]
public void TestCorruptAudioFile()
{
using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateBadFormat);
}
}
private BeatmapVerifierContext getContext(Stream resourceStream, bool allowMissing = false)
{
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
}
}
}

View File

@ -0,0 +1,86 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using FileInfo = osu.Game.IO.FileInfo;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckZeroByteFilesTest
{
private CheckZeroByteFiles check;
private IBeatmap beatmap;
[SetUp]
public void Setup()
{
check = new CheckZeroByteFiles();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = new BeatmapSetInfo
{
Files = new List<BeatmapSetFileInfo>(new[]
{
new BeatmapSetFileInfo
{
Filename = "abc123.jpg",
FileInfo = new FileInfo { Hash = "abcdef" }
}
})
}
}
};
}
[Test]
public void TestNonZeroBytes()
{
Assert.IsEmpty(check.Run(getContext(byteLength: 44)));
}
[Test]
public void TestZeroBytes()
{
var issues = check.Run(getContext(byteLength: 0)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckZeroByteFiles.IssueTemplateZeroBytes);
}
[Test]
public void TestMissing()
{
Assert.IsEmpty(check.Run(getContextMissing()));
}
private BeatmapVerifierContext getContext(long byteLength)
{
var mockStream = new Mock<Stream>();
mockStream.Setup(s => s.Length).Returns(byteLength);
var mockWorkingBeatmap = new Mock<IWorkingBeatmap>();
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(mockStream.Object);
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
}
private BeatmapVerifierContext getContextMissing()
{
var mockWorkingBeatmap = new Mock<IWorkingBeatmap>();
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns((Stream)null);
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
}
}
}

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit;
@ -55,8 +56,6 @@ namespace osu.Game.Tests.Editing
composer.EditorBeatmap.Difficulty.SliderMultiplier = 1;
composer.EditorBeatmap.ControlPointInfo.Clear();
composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 1 });
composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 });
});
@ -73,13 +72,13 @@ namespace osu.Game.Tests.Editing
[TestCase(2)]
public void TestSpeedMultiplier(float multiplier)
{
AddStep($"set multiplier = {multiplier}", () =>
assertSnapDistance(100 * multiplier, new HitObject
{
composer.EditorBeatmap.ControlPointInfo.Clear();
composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = multiplier });
DifficultyControlPoint = new DifficultyControlPoint
{
SliderVelocity = multiplier
}
});
assertSnapDistance(100 * multiplier);
}
[TestCase(1)]
@ -197,20 +196,20 @@ namespace osu.Game.Tests.Editing
assertSnappedDistance(400, 400);
}
private void assertSnapDistance(float expectedDistance)
=> AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(0) == expectedDistance);
private void assertSnapDistance(float expectedDistance, HitObject hitObject = null)
=> AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(hitObject ?? new HitObject()) == expectedDistance);
private void assertDurationToDistance(double duration, float expectedDistance)
=> AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(0, duration) == expectedDistance);
=> AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(new HitObject(), duration) == expectedDistance);
private void assertDistanceToDuration(float distance, double expectedDuration)
=> AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(0, distance) == expectedDuration);
=> AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(new HitObject(), distance) == expectedDuration);
private void assertSnappedDuration(float distance, double expectedDuration)
=> AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.GetSnappedDurationFromDistance(0, distance) == expectedDuration);
=> AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.GetSnappedDurationFromDistance(new HitObject(), distance) == expectedDuration);
private void assertSnappedDistance(float distance, float expectedDistance)
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.GetSnappedDistanceFromDistance(0, distance) == expectedDistance);
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.GetSnappedDistanceFromDistance(new HitObject(), distance) == expectedDistance);
private class TestHitObjectComposer : OsuHitObjectComposer
{

View File

@ -17,7 +17,7 @@ namespace osu.Game.Tests
protected virtual TestOsuGameBase LoadOsuIntoHost(GameHost host, bool withBeatmap = false)
{
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);
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");

View File

@ -46,7 +46,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestAddRedundantDifficulty()
{
var cpi = new ControlPointInfo();
var cpi = new LegacyControlPointInfo();
cpi.Add(0, new DifficultyControlPoint()); // is redundant
cpi.Add(1000, new DifficultyControlPoint()); // is redundant
@ -55,7 +55,7 @@ namespace osu.Game.Tests.NonVisual
Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(0));
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
cpi.Add(1000, new DifficultyControlPoint { SpeedMultiplier = 2 }); // is not redundant
cpi.Add(1000, new DifficultyControlPoint { SliderVelocity = 2 }); // is not redundant
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1));
@ -159,7 +159,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestAddControlPointToGroup()
{
var cpi = new ControlPointInfo();
var cpi = new LegacyControlPointInfo();
var group = cpi.GroupAt(1000, true);
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
@ -174,23 +174,23 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestAddDuplicateControlPointToGroup()
{
var cpi = new ControlPointInfo();
var cpi = new LegacyControlPointInfo();
var group = cpi.GroupAt(1000, true);
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
group.Add(new DifficultyControlPoint());
group.Add(new DifficultyControlPoint { SpeedMultiplier = 2 });
group.Add(new DifficultyControlPoint { SliderVelocity = 2 });
Assert.That(group.ControlPoints.Count, Is.EqualTo(1));
Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1));
Assert.That(cpi.DifficultyPoints.First().SpeedMultiplier, Is.EqualTo(2));
Assert.That(cpi.DifficultyPoints.First().SliderVelocity, Is.EqualTo(2));
}
[Test]
public void TestRemoveControlPointFromGroup()
{
var cpi = new ControlPointInfo();
var cpi = new LegacyControlPointInfo();
var group = cpi.GroupAt(1000, true);
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
@ -208,14 +208,14 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestOrdering()
{
var cpi = new ControlPointInfo();
var cpi = new LegacyControlPointInfo();
cpi.Add(0, new TimingControlPoint());
cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
cpi.Add(10000, new TimingControlPoint { BeatLength = 200 });
cpi.Add(5000, new TimingControlPoint { BeatLength = 100 });
cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 });
cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 });
cpi.Add(3000, new DifficultyControlPoint { SliderVelocity = 2 });
cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SliderVelocity = 4 });
cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 });
cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true });
@ -230,14 +230,14 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestClear()
{
var cpi = new ControlPointInfo();
var cpi = new LegacyControlPointInfo();
cpi.Add(0, new TimingControlPoint());
cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
cpi.Add(10000, new TimingControlPoint { BeatLength = 200 });
cpi.Add(5000, new TimingControlPoint { BeatLength = 100 });
cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 });
cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 });
cpi.Add(3000, new DifficultyControlPoint { SliderVelocity = 2 });
cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SliderVelocity = 4 });
cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 });
cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true });

View File

@ -168,14 +168,14 @@ namespace osu.Game.Tests.Online
return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, api, host);
}
protected override BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host)
protected override BeatmapModelDownloader CreateBeatmapModelDownloader(IBeatmapModelManager manager, IAPIProvider api, GameHost host)
{
return new TestBeatmapModelDownloader(modelManager, api, host);
return new TestBeatmapModelDownloader(manager, api, host);
}
internal class TestBeatmapModelDownloader : BeatmapModelDownloader
{
public TestBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider apiProvider, GameHost gameHost)
public TestBeatmapModelDownloader(IBeatmapModelManager modelManager, IAPIProvider apiProvider, GameHost gameHost)
: base(modelManager, apiProvider, gameHost)
{
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Binary file not shown.

View File

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

View File

@ -6,6 +6,7 @@ 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;
@ -18,16 +19,19 @@ namespace osu.Game.Tests.Visual.Audio
{
public class TestSceneAudioFilter : OsuTestScene
{
private OsuSpriteText lowpassText;
private AudioFilter lowpassFilter;
private OsuSpriteText lowPassText;
private AudioFilter lowPassFilter;
private OsuSpriteText highpassText;
private AudioFilter highpassFilter;
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)
{
@ -38,53 +42,89 @@ namespace osu.Game.Tests.Visual.Audio
{
Children = new Drawable[]
{
lowpassFilter = new AudioFilter(audio.TrackMixer),
highpassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
lowpassText = new OsuSpriteText
lowPassFilter = new AudioFilter(audio.TrackMixer),
highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
lowPassText = new OsuSpriteText
{
Padding = new MarginPadding(20),
Text = $"Low Pass: {lowpassFilter.Cutoff.Value}hz",
Text = $"Low Pass: {lowPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40)
},
new OsuSliderBar<int>
lowPassSlider = new OsuSliderBar<int>
{
Width = 500,
Height = 50,
Padding = new MarginPadding(20),
Current = { BindTarget = lowpassFilter.Cutoff }
Current = new BindableInt
{
MinValue = 0,
MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
}
},
highpassText = new OsuSpriteText
highPassText = new OsuSpriteText
{
Padding = new MarginPadding(20),
Text = $"High Pass: {highpassFilter.Cutoff.Value}hz",
Text = $"High Pass: {highPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40)
},
new OsuSliderBar<int>
highPassSlider = new OsuSliderBar<int>
{
Width = 500,
Height = 50,
Padding = new MarginPadding(20),
Current = { BindTarget = highpassFilter.Cutoff }
Current = new BindableInt
{
MinValue = 0,
MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
}
}
}
});
lowpassFilter.Cutoff.ValueChanged += e => lowpassText.Text = $"Low Pass: {e.NewValue}hz";
highpassFilter.Cutoff.ValueChanged += e => highpassText.Text = $"High Pass: {e.NewValue}hz";
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 TestLowPass()
public void TestLowPassSweep()
{
AddStep("Filter Sweep", () =>
{
lowpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic);
});
@ -92,7 +132,7 @@ namespace osu.Game.Tests.Visual.Audio
AddStep("Filter Sweep (reverse)", () =>
{
lowpassFilter.CutoffTo(0).Then()
lowPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
});
@ -101,11 +141,11 @@ namespace osu.Game.Tests.Visual.Audio
}
[Test]
public void TestHighPass()
public void TestHighPassSweep()
{
AddStep("Filter Sweep", () =>
{
highpassFilter.CutoffTo(0).Then()
highPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
});
@ -113,7 +153,7 @@ namespace osu.Game.Tests.Visual.Audio
AddStep("Filter Sweep (reverse)", () =>
{
highpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
highPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic);
});
@ -123,5 +163,11 @@ namespace osu.Game.Tests.Visual.Audio
}
private void waitTrackPlay() => AddWaitStep("Let track play", 10);
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
track?.Dispose();
}
}
}

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
@ -81,7 +82,7 @@ namespace osu.Game.Tests.Visual.Editing
public new float DistanceSpacing => base.DistanceSpacing;
public TestDistanceSnapGrid(double? endTime = null)
: base(grid_position, 0, endTime)
: base(new HitObject(), grid_position, 0, endTime)
{
}
@ -158,15 +159,15 @@ namespace osu.Game.Tests.Visual.Editing
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
public float GetBeatSnapDistanceAt(double referenceTime) => 10;
public float GetBeatSnapDistanceAt(HitObject referenceObject) => 10;
public float DurationToDistance(double referenceTime, double duration) => (float)duration;
public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration;
public double DistanceToDuration(double referenceTime, float distance) => distance;
public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0;
public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0;
public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0;
}
}
}

View File

@ -8,6 +8,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osuTK.Input;
@ -30,22 +31,36 @@ namespace osu.Game.Tests.Visual.Editing
PushAndConfirm(() => new EditorLoader());
AddUntilStep("wait for editor load", () => editor != null);
AddUntilStep("wait for editor load", () => editor?.IsLoaded == true);
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddUntilStep("wait for metadata screen load", () => editor.ChildrenOfType<MetadataSection>().FirstOrDefault()?.IsLoaded == true);
// We intentionally switch away from the metadata screen, else there is a feedback loop with the textbox handling which causes metadata changes below to get overwritten.
AddStep("Enter compose mode", () => InputManager.Key(Key.F1));
AddUntilStep("Wait for compose mode load", () => editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true);
AddStep("Set overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty = 7);
AddStep("Set artist and title", () =>
{
editorBeatmap.BeatmapInfo.Metadata.Artist = "artist";
editorBeatmap.BeatmapInfo.Metadata.Title = "title";
});
AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.Version = "difficulty");
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddStep("Change to placement mode", () => InputManager.Key(Key.Number2));
AddStep("Move to playfield", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre));
AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left));
AddStep("Save and exit", () =>
{
InputManager.Keys(PlatformAction.Save);
InputManager.Key(Key.Escape);
});
checkMutations();
AddStep("Save", () => InputManager.Keys(PlatformAction.Save));
checkMutations();
AddStep("Exit", () => InputManager.Key(Key.Escape));
AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
@ -56,7 +71,16 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Enter editor", () => InputManager.Key(Key.Number5));
AddUntilStep("Wait for editor load", () => editor != null);
checkMutations();
}
private void checkMutations()
{
AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1);
AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7);
AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title");
AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.Version == "difficulty");
}
}
}

View File

@ -103,6 +103,30 @@ namespace osu.Game.Tests.Visual.Gameplay
checkFrameCount(0);
}
[Test]
public void TestRatePreservedWhenTimeNotProgressing()
{
AddStep("set manual clock rate", () => manualClock.Rate = 1);
seekManualTo(5000);
createStabilityContainer();
checkRate(1);
seekManualTo(10000);
checkRate(1);
AddWaitStep("wait some", 3);
checkRate(1);
seekManualTo(5000);
checkRate(-1);
AddWaitStep("wait some", 3);
checkRate(-1);
seekManualTo(10000);
checkRate(1);
}
private const int max_frames_catchup = 50;
private void createStabilityContainer(double gameplayStartTime = double.MinValue) => AddStep("create container", () =>
@ -116,6 +140,9 @@ namespace osu.Game.Tests.Visual.Gameplay
private void checkFrameCount(int frames) =>
AddAssert($"elapsed frames is {frames}", () => consumer.ElapsedFrames == frames);
private void checkRate(double rate) =>
AddAssert($"clock rate is {rate}", () => consumer.Clock.Rate == rate);
public class ClockConsumingChild : CompositeDrawable
{
private readonly OsuSpriteText text;

View File

@ -93,9 +93,9 @@ namespace osu.Game.Tests.Visual.Gameplay
private IList<MultiplierControlPoint> testControlPoints => new List<MultiplierControlPoint>
{
new MultiplierControlPoint(time_range) { DifficultyPoint = { SpeedMultiplier = 1.25 } },
new MultiplierControlPoint(1.5 * time_range) { DifficultyPoint = { SpeedMultiplier = 1 } },
new MultiplierControlPoint(2 * time_range) { DifficultyPoint = { SpeedMultiplier = 1.5 } }
new MultiplierControlPoint(time_range) { EffectPoint = { ScrollSpeed = 1.25 } },
new MultiplierControlPoint(1.5 * time_range) { EffectPoint = { ScrollSpeed = 1 } },
new MultiplierControlPoint(2 * time_range) { EffectPoint = { ScrollSpeed = 1.5 } }
};
[Test]

View File

@ -90,8 +90,12 @@ namespace osu.Game.Tests.Visual.Gameplay
CreateTest(() =>
{
AddStep("fail on first judgement", () => currentFailConditions = (_, __) => true);
AddStep("set storyboard duration to 1.3s", () => currentStoryboardDuration = 1300);
// Fail occurs at 164ms with the provided beatmap.
// Fail animation runs for 2.5s realtime but the gameplay time change is *variable* due to the frequency transform being applied, so we need a bit of lenience.
AddStep("set storyboard duration to 0.6s", () => currentStoryboardDuration = 600);
});
AddUntilStep("wait for fail", () => Player.HasFailed);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);

View File

@ -564,11 +564,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
AddRepeatStep("click spectate button", () =>
AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType<OsuButton>().Single().Enabled.Value);
AddStep("click ready button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerReadyButton>().Single());
InputManager.MoveMouseTo(readyButton);
InputManager.Click(MouseButton.Left);
}, 2);
});
AddUntilStep("wait for player to be ready", () => client.Room?.Users[0].State == MultiplayerUserState.Ready);
AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType<OsuButton>().Single().Enabled.Value);
AddStep("click start button", () => InputManager.Click(MouseButton.Left));
AddUntilStep("wait for player", () => Stack.CurrentScreen is Player);
@ -582,6 +589,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for results", () => Stack.CurrentScreen is ResultsScreen);
}
private MultiplayerReadyButton readyButton => this.ChildrenOfType<MultiplayerReadyButton>().Single();
private void createRoom(Func<Room> room)
{
AddUntilStep("wait for lounge", () => multiplayerScreen.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true);

View File

@ -0,0 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Overlays.Notifications;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Navigation
{
public class TestSceneStartupImport : OsuGameTestScene
{
private string importFilename;
protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { importFilename });
public override void SetUpSteps()
{
AddStep("Prepare import beatmap", () => importFilename = TestResources.GetTestBeatmapForImport());
base.SetUpSteps();
}
[Test]
public void TestImportCreatedNotification()
{
AddUntilStep("Import notification was presented", () => Game.Notifications.ChildrenOfType<ProgressCompletionNotification>().Count() == 1);
}
}
}

View File

@ -32,12 +32,14 @@ namespace osu.Game.Tests.Visual.Playlists
private TestResultsScreen resultsScreen;
private int currentScoreId;
private bool requestComplete;
private int totalCount;
[SetUp]
public void Setup() => Schedule(() =>
{
currentScoreId = 0;
requestComplete = false;
totalCount = 0;
bindHandler();
});
@ -53,7 +55,6 @@ namespace osu.Game.Tests.Visual.Playlists
});
createResults(() => userScore);
waitForDisplay();
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
}
@ -62,7 +63,6 @@ namespace osu.Game.Tests.Visual.Playlists
public void TestShowNullUserScore()
{
createResults();
waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
}
@ -79,7 +79,6 @@ namespace osu.Game.Tests.Visual.Playlists
});
createResults(() => userScore);
waitForDisplay();
AddAssert("more than 1 panel displayed", () => this.ChildrenOfType<ScorePanel>().Count() > 1);
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
@ -91,7 +90,6 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("bind delayed handler", () => bindHandler(true));
createResults();
waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
}
@ -100,7 +98,6 @@ namespace osu.Game.Tests.Visual.Playlists
public void TestFetchWhenScrolledToTheRight()
{
createResults();
waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(true));
@ -131,7 +128,6 @@ namespace osu.Game.Tests.Visual.Playlists
});
createResults(() => userScore);
waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(true));
@ -161,13 +157,15 @@ namespace osu.Game.Tests.Visual.Playlists
}));
});
AddUntilStep("wait for load", () => resultsScreen.ChildrenOfType<ScorePanelList>().FirstOrDefault()?.AllPanelsVisible == true);
waitForDisplay();
}
private void waitForDisplay()
{
AddUntilStep("wait for request to complete", () => requestComplete);
AddUntilStep("wait for panels to be visible", () => resultsScreen.ChildrenOfType<ScorePanelList>().FirstOrDefault()?.AllPanelsVisible == true);
AddUntilStep("wait for load to complete", () =>
requestComplete
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount
&& resultsScreen.ScorePanelList.AllPanelsVisible);
AddWaitStep("wait for display", 5);
}
@ -203,6 +201,7 @@ namespace osu.Game.Tests.Visual.Playlists
triggerFail(s);
else
triggerSuccess(s, createUserResponse(userScore));
break;
case IndexPlaylistScoresRequest i:
@ -248,6 +247,8 @@ namespace osu.Game.Tests.Visual.Playlists
}
};
totalCount++;
for (int i = 1; i <= scores_per_result; i++)
{
multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore
@ -285,6 +286,8 @@ namespace osu.Game.Tests.Visual.Playlists
},
Statistics = userScore.Statistics
});
totalCount += 2;
}
addCursor(multiplayerUserScore.ScoresAround.Lower);
@ -325,6 +328,8 @@ namespace osu.Game.Tests.Visual.Playlists
{ HitResult.Great, 300 }
}
});
totalCount++;
}
addCursor(result);

View File

@ -3,6 +3,7 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Handlers.Tablet;
@ -21,6 +22,9 @@ namespace osu.Game.Tests.Visual.Settings
private TestTabletHandler tabletHandler;
private TabletSettings settings;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
[SetUpSteps]
public void SetUpSteps()
{

View File

@ -205,7 +205,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) =>
AddAssert($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'",
// A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872
() => shouldContain == (getCollectionDropdownItems().Any(i => i.ChildrenOfType<FillFlowContainer>().OfType<IHasText>().First().Text == collectionName)));
() => shouldContain == (getCollectionDropdownItems().Any(i => i.ChildrenOfType<CompositeDrawable>().OfType<IHasText>().First().Text == collectionName)));
private IconButton getAddOrRemoveButton(int index)
=> getCollectionDropdownItems().ElementAt(index).ChildrenOfType<IconButton>().Single();

View File

@ -142,6 +142,8 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("store selected beatmap", () => selected = Beatmap.Value);
AddUntilStep("wait for beatmaps to load", () => songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmap>().Any());
AddStep("select next and enter", () =>
{
InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmap>()
@ -599,10 +601,10 @@ namespace osu.Game.Tests.Visual.SongSelect
});
FilterableDifficultyIcon difficultyIcon = null;
AddStep("Find an icon", () =>
AddUntilStep("Find an icon", () =>
{
difficultyIcon = set.ChildrenOfType<FilterableDifficultyIcon>()
.First(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex());
return (difficultyIcon = set.ChildrenOfType<FilterableDifficultyIcon>()
.FirstOrDefault(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex())) != null;
});
AddStep("Click on a difficulty", () =>
@ -765,10 +767,10 @@ namespace osu.Game.Tests.Visual.SongSelect
});
FilterableGroupedDifficultyIcon groupIcon = null;
AddStep("Find group icon for different ruleset", () =>
AddUntilStep("Find group icon for different ruleset", () =>
{
groupIcon = set.ChildrenOfType<FilterableGroupedDifficultyIcon>()
.First(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3);
return (groupIcon = set.ChildrenOfType<FilterableGroupedDifficultyIcon>()
.FirstOrDefault(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3)) != null;
});
AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0);

View File

@ -163,7 +163,6 @@ namespace osu.Game.Tests.Visual.UserInterface
});
AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scoreBeingDeleted.OnlineScoreID));
}
@ -171,6 +170,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestDeleteViaDatabase()
{
AddStep("delete top score", () => scoreManager.Delete(importedScores[0]));
AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != importedScores[0].OnlineScoreID));
}
}

View File

@ -36,9 +36,9 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod));
AddAssert(@"Check DoubleTime multiplier", () => assertModsMultiplier(doubleTimeMod));
var mutlipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() };
AddStep(@"Add multiple Mods", () => changeMods(mutlipleIncrementMods));
AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(mutlipleIncrementMods));
var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() };
AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods));
AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleIncrementMods));
}
[Test]

View File

@ -1,11 +1,15 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface
{
@ -19,28 +23,62 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("create component", () =>
{
LabelledSliderBar<double> component;
FillFlowContainer flow;
Child = new Container
Child = flow = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
AutoSizeAxes = Axes.Y,
Child = component = new LabelledSliderBar<double>
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Current = new BindableDouble(5)
new LabelledSliderBar<double>
{
MinValue = 0,
MaxValue = 10,
Precision = 1,
}
}
Current = new BindableDouble(5)
{
MinValue = 0,
MaxValue = 10,
Precision = 1,
},
Label = "a sample component",
Description = hasDescription ? "this text describes the component" : string.Empty,
},
},
};
component.Label = "a sample component";
component.Description = hasDescription ? "this text describes the component" : string.Empty;
foreach (var colour in Enum.GetValues(typeof(OverlayColourScheme)).OfType<OverlayColourScheme>())
{
flow.Add(new OverlayColourContainer(colour)
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = new LabelledSliderBar<double>
{
Current = new BindableDouble(5)
{
MinValue = 0,
MaxValue = 10,
Precision = 1,
},
Label = "a sample component",
Description = hasDescription ? "this text describes the component" : string.Empty,
}
});
}
});
}
private class OverlayColourContainer : Container
{
[Cached]
private OverlayColourProvider colourProvider;
public OverlayColourContainer(OverlayColourScheme scheme)
{
colourProvider = new OverlayColourProvider(scheme);
}
}
}
}

View File

@ -0,0 +1,44 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneRoundedButton : OsuTestScene
{
[Test]
public void TestBasic()
{
RoundedButton button = null;
AddStep("create button", () => Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.DarkGray
},
button = new RoundedButton
{
Width = 400,
Text = "Test button",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Action = () => { }
}
}
});
AddToggleStep("toggle disabled", disabled => button.Action = disabled ? (Action)null : () => { });
}
}
}

View File

@ -0,0 +1,68 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneSettingsCheckbox : OsuTestScene
{
[TestCase]
public void TestCheckbox()
{
AddStep("create component", () =>
{
FillFlowContainer flow;
Child = flow = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(5),
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new SettingsCheckbox
{
LabelText = "a sample component",
},
},
};
foreach (var colour1 in Enum.GetValues(typeof(OverlayColourScheme)).OfType<OverlayColourScheme>())
{
flow.Add(new OverlayColourContainer(colour1)
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = new SettingsCheckbox
{
LabelText = "a sample component",
}
});
}
});
}
private class OverlayColourContainer : Container
{
[Cached]
private OverlayColourProvider colourProvider;
public OverlayColourContainer(OverlayColourScheme scheme)
{
colourProvider = new OverlayColourProvider(scheme);
}
}
}
}

View File

@ -14,7 +14,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
public static TournamentGameBase LoadTournament(GameHost host, TournamentGameBase tournament = null)
{
tournament ??= new TournamentGameBase();
Task.Run(() => host.Run(tournament))
Task.Factory.StartNew(() => host.Run(tournament), TaskCreationOptions.LongRunning)
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
WaitForOrAssert(() => tournament.IsLoaded, @"osu! failed to start in a reasonable amount of time");
return tournament;

View File

@ -127,7 +127,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
score2Text.X = Math.Max(5 + score2Text.DrawWidth / 2, score2Bar.DrawWidth);
}
private class MatchScoreCounter : ScoreCounter
private class MatchScoreCounter : CommaSeparatedScoreCounter
{
private OsuSpriteText displayedSpriteText;

View File

@ -4,7 +4,6 @@
using System.Diagnostics;
using ManagedBass.Fx;
using osu.Framework.Audio.Mixing;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Audio.Effects
@ -21,10 +20,25 @@ namespace osu.Game.Audio.Effects
private readonly BQFParameters filter;
private readonly BQFType type;
private bool isAttached;
private int cutoff;
/// <summary>
/// The current cutoff of this filter.
/// The cutoff frequency of this filter.
/// </summary>
public BindableNumber<int> Cutoff { get; }
public int Cutoff
{
get => cutoff;
set
{
if (value == cutoff)
return;
cutoff = value;
updateFilter(cutoff);
}
}
/// <summary>
/// A Component that implements a BASS FX BiQuad Filter Effect.
@ -36,102 +50,96 @@ namespace osu.Game.Audio.Effects
this.mixer = mixer;
this.type = type;
int initialCutoff;
switch (type)
{
case BQFType.HighPass:
initialCutoff = 1;
break;
case BQFType.LowPass:
initialCutoff = MAX_LOWPASS_CUTOFF;
break;
default:
initialCutoff = 500; // A default that should ensure audio remains audible for other filters.
break;
}
Cutoff = new BindableNumber<int>(initialCutoff)
{
MinValue = 1,
MaxValue = MAX_LOWPASS_CUTOFF
};
filter = new BQFParameters
{
lFilter = type,
fCenter = initialCutoff,
fBandwidth = 0,
fQ = 0.7f // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0)
// This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0)
fQ = 0.7f
};
// Don't start attached if this is low-pass or high-pass filter (as they have special auto-attach/detach logic)
if (type != BQFType.LowPass && type != BQFType.HighPass)
attachFilter();
Cutoff.ValueChanged += updateFilter;
Cutoff = getInitialCutoff(type);
}
private void attachFilter()
private int getInitialCutoff(BQFType type)
{
Debug.Assert(!mixer.Effects.Contains(filter));
mixer.Effects.Add(filter);
}
private void detachFilter()
{
Debug.Assert(mixer.Effects.Contains(filter));
mixer.Effects.Remove(filter);
}
private void updateFilter(ValueChangedEvent<int> cutoff)
{
// Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz.
if (type == BQFType.LowPass)
switch (type)
{
if (cutoff.NewValue >= MAX_LOWPASS_CUTOFF)
{
detachFilter();
return;
}
case BQFType.HighPass:
return 1;
if (cutoff.OldValue >= MAX_LOWPASS_CUTOFF && cutoff.NewValue < MAX_LOWPASS_CUTOFF)
attachFilter();
case BQFType.LowPass:
return MAX_LOWPASS_CUTOFF;
default:
return 500; // A default that should ensure audio remains audible for other filters.
}
}
private void updateFilter(int newValue)
{
switch (type)
{
case BQFType.LowPass:
// Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz.
if (newValue >= MAX_LOWPASS_CUTOFF)
{
ensureDetached();
return;
}
break;
// Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz.
case BQFType.HighPass:
if (newValue <= 1)
{
ensureDetached();
return;
}
break;
}
// Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz.
if (type == BQFType.HighPass)
{
if (cutoff.NewValue <= 1)
{
detachFilter();
return;
}
if (cutoff.OldValue <= 1 && cutoff.NewValue > 1)
attachFilter();
}
ensureAttached();
var filterIndex = mixer.Effects.IndexOf(filter);
if (filterIndex < 0) return;
if (mixer.Effects[filterIndex] is BQFParameters existingFilter)
{
existingFilter.fCenter = cutoff.NewValue;
existingFilter.fCenter = newValue;
// required to update effect with new parameters.
mixer.Effects[filterIndex] = existingFilter;
}
}
private void ensureAttached()
{
if (isAttached)
return;
Debug.Assert(!mixer.Effects.Contains(filter));
mixer.Effects.Add(filter);
isAttached = true;
}
private void ensureDetached()
{
if (!isAttached)
return;
Debug.Assert(mixer.Effects.Contains(filter));
mixer.Effects.Remove(filter);
isAttached = false;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (mixer.Effects.Contains(filter))
detachFilter();
ensureDetached();
}
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
@ -12,7 +11,7 @@ namespace osu.Game.Audio.Effects
/// <summary>
/// The filter cutoff.
/// </summary>
BindableNumber<int> Cutoff { get; }
int Cutoff { get; set; }
}
public static class FilterableAudioComponentExtensions
@ -40,7 +39,7 @@ namespace osu.Game.Audio.Effects
public static TransformSequence<T> CutoffTo<T, TEasing>(this T component, int newCutoff, double duration, TEasing easing)
where T : class, ITransformableFilter, IDrawable
where TEasing : IEasingFunction
=> component.TransformBindableTo(component.Cutoff, newCutoff, duration, easing);
=> component.TransformTo(nameof(component.Cutoff), newCutoff, duration, easing);
/// <summary>
/// Smoothly adjusts filter cutoff over time.
@ -49,6 +48,6 @@ namespace osu.Game.Audio.Effects
public static TransformSequence<T> CutoffTo<T, TEasing>(this TransformSequence<T> sequence, int newCutoff, double duration, TEasing easing)
where T : class, ITransformableFilter, IDrawable
where TEasing : IEasingFunction
=> sequence.Append(o => o.TransformBindableTo(o.Cutoff, newCutoff, duration, easing));
=> sequence.Append(o => o.TransformTo(nameof(o.Cutoff), newCutoff, duration, easing));
}
}

View File

@ -40,7 +40,13 @@ namespace osu.Game.Beatmaps
public IBeatmap Convert(CancellationToken cancellationToken = default)
{
// We always operate on a clone of the original beatmap, to not modify it game-wide
return ConvertBeatmap(Beatmap.Clone(), cancellationToken);
var original = Beatmap.Clone();
// Shallow clone isn't enough to ensure we don't mutate beatmap info unexpectedly.
// Can potentially be removed after `Beatmap.Difficulty` doesn't save back to `Beatmap.BeatmapInfo`.
original.BeatmapInfo = original.BeatmapInfo.Clone();
return ConvertBeatmap(original, cancellationToken);
}
/// <summary>

View File

@ -54,7 +54,7 @@ namespace osu.Game.Beatmaps
}
}
protected virtual BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host)
protected virtual BeatmapModelDownloader CreateBeatmapModelDownloader(IBeatmapModelManager modelManager, IAPIProvider api, GameHost host)
{
return new BeatmapModelDownloader(modelManager, api, host);
}
@ -176,11 +176,6 @@ namespace osu.Game.Beatmaps
}
}
/// <summary>
/// Fired when the user requests to view the resulting import.
/// </summary>
public Action<IEnumerable<ILive<BeatmapSetInfo>>> PresentImport { set => beatmapModelManager.PostImport = value; }
/// <summary>
/// Delete a beatmap difficulty.
/// </summary>
@ -338,5 +333,14 @@ namespace osu.Game.Beatmaps
}
#endregion
#region Implementation of IPostImports<out BeatmapSetInfo>
public Action<IEnumerable<ILive<BeatmapSetInfo>>> PostImport
{
set => beatmapModelManager.PostImport = value;
}
#endregion
}
}

View File

@ -13,7 +13,7 @@ namespace osu.Game.Beatmaps
protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) =>
new DownloadBeatmapSetRequest(set, minimiseDownloadSize);
public BeatmapModelDownloader(BeatmapModelManager beatmapModelManager, IAPIProvider api, GameHost host = null)
public BeatmapModelDownloader(IBeatmapModelManager beatmapModelManager, IAPIProvider api, GameHost host = null)
: base(beatmapModelManager, api, host)
{
}

View File

@ -192,6 +192,13 @@ namespace osu.Game.Beatmaps
{
var setInfo = beatmapInfo.BeatmapSet;
// Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
// This should hopefully be temporary, assuming said clone is eventually removed.
beatmapInfo.BaseDifficulty.CopyFrom(beatmapContent.Difficulty);
// All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
beatmapContent.BeatmapInfo = beatmapInfo;
using (var stream = new MemoryStream())
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
@ -202,7 +209,6 @@ namespace osu.Game.Beatmaps
using (ContextFactory.GetForWrite())
{
beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == beatmapInfo.ID);
beatmapInfo.BaseDifficulty.CopyFrom(beatmapContent.Difficulty);
var metadata = beatmapInfo.Metadata ?? setInfo.Metadata;

View File

@ -15,11 +15,9 @@ namespace osu.Game.Beatmaps.ControlPoints
/// The time at which the control point takes effect.
/// </summary>
[JsonIgnore]
public double Time => controlPointGroup?.Time ?? 0;
public double Time { get; set; }
private ControlPointGroup controlPointGroup;
public void AttachGroup(ControlPointGroup pointGroup) => controlPointGroup = pointGroup;
public void AttachGroup(ControlPointGroup pointGroup) => Time = pointGroup.Time;
public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time);
@ -46,6 +44,7 @@ namespace osu.Game.Beatmaps.ControlPoints
public virtual void CopyFrom(ControlPoint other)
{
Time = other.Time;
}
}
}

View File

@ -33,14 +33,6 @@ namespace osu.Game.Beatmaps.ControlPoints
private readonly SortedList<TimingControlPoint> timingPoints = new SortedList<TimingControlPoint>(Comparer<TimingControlPoint>.Default);
/// <summary>
/// All difficulty points.
/// </summary>
[JsonProperty]
public IReadOnlyList<DifficultyControlPoint> DifficultyPoints => difficultyPoints;
private readonly SortedList<DifficultyControlPoint> difficultyPoints = new SortedList<DifficultyControlPoint>(Comparer<DifficultyControlPoint>.Default);
/// <summary>
/// All effect points.
/// </summary>
@ -55,14 +47,6 @@ namespace osu.Game.Beatmaps.ControlPoints
[JsonIgnore]
public IEnumerable<ControlPoint> AllControlPoints => Groups.SelectMany(g => g.ControlPoints).ToArray();
/// <summary>
/// Finds the difficulty control point that is active at <paramref name="time"/>.
/// </summary>
/// <param name="time">The time to find the difficulty control point at.</param>
/// <returns>The difficulty control point.</returns>
[NotNull]
public DifficultyControlPoint DifficultyPointAt(double time) => BinarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT);
/// <summary>
/// Finds the effect control point that is active at <paramref name="time"/>.
/// </summary>
@ -100,7 +84,6 @@ namespace osu.Game.Beatmaps.ControlPoints
{
groups.Clear();
timingPoints.Clear();
difficultyPoints.Clear();
effectPoints.Clear();
}
@ -277,10 +260,6 @@ namespace osu.Game.Beatmaps.ControlPoints
case EffectControlPoint _:
existing = EffectPointAt(time);
break;
case DifficultyControlPoint _:
existing = DifficultyPointAt(time);
break;
}
return newPoint?.IsRedundant(existing) == true;
@ -298,9 +277,8 @@ namespace osu.Game.Beatmaps.ControlPoints
effectPoints.Add(typed);
break;
case DifficultyControlPoint typed:
difficultyPoints.Add(typed);
break;
default:
throw new ArgumentException($"A control point of unexpected type {controlPoint.GetType()} was added to this {nameof(ControlPointInfo)}");
}
}
@ -315,10 +293,6 @@ namespace osu.Game.Beatmaps.ControlPoints
case EffectControlPoint typed:
effectPoints.Remove(typed);
break;
case DifficultyControlPoint typed:
difficultyPoints.Remove(typed);
break;
}
}

View File

@ -7,17 +7,20 @@ using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints
{
/// <remarks>
/// Note that going forward, this control point type should always be assigned directly to HitObjects.
/// </remarks>
public class DifficultyControlPoint : ControlPoint
{
public static readonly DifficultyControlPoint DEFAULT = new DifficultyControlPoint
{
SpeedMultiplierBindable = { Disabled = true },
SliderVelocityBindable = { Disabled = true },
};
/// <summary>
/// The speed multiplier at this control point.
/// The slider velocity at this control point.
/// </summary>
public readonly BindableDouble SpeedMultiplierBindable = new BindableDouble(1)
public readonly BindableDouble SliderVelocityBindable = new BindableDouble(1)
{
Precision = 0.01,
Default = 1,
@ -28,21 +31,21 @@ namespace osu.Game.Beatmaps.ControlPoints
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1;
/// <summary>
/// The speed multiplier at this control point.
/// The slider velocity at this control point.
/// </summary>
public double SpeedMultiplier
public double SliderVelocity
{
get => SpeedMultiplierBindable.Value;
set => SpeedMultiplierBindable.Value = value;
get => SliderVelocityBindable.Value;
set => SliderVelocityBindable.Value = value;
}
public override bool IsRedundant(ControlPoint existing)
=> existing is DifficultyControlPoint existingDifficulty
&& SpeedMultiplier == existingDifficulty.SpeedMultiplier;
&& SliderVelocity == existingDifficulty.SliderVelocity;
public override void CopyFrom(ControlPoint other)
{
SpeedMultiplier = ((DifficultyControlPoint)other).SpeedMultiplier;
SliderVelocity = ((DifficultyControlPoint)other).SliderVelocity;
base.CopyFrom(other);
}

View File

@ -12,7 +12,8 @@ namespace osu.Game.Beatmaps.ControlPoints
public static readonly EffectControlPoint DEFAULT = new EffectControlPoint
{
KiaiModeBindable = { Disabled = true },
OmitFirstBarLineBindable = { Disabled = true }
OmitFirstBarLineBindable = { Disabled = true },
ScrollSpeedBindable = { Disabled = true }
};
/// <summary>
@ -20,6 +21,26 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary>
public readonly BindableBool OmitFirstBarLineBindable = new BindableBool();
/// <summary>
/// The relative scroll speed at this control point.
/// </summary>
public readonly BindableDouble ScrollSpeedBindable = new BindableDouble(1)
{
Precision = 0.01,
Default = 1,
MinValue = 0.01,
MaxValue = 10
};
/// <summary>
/// The relative scroll speed.
/// </summary>
public double ScrollSpeed
{
get => ScrollSpeedBindable.Value;
set => ScrollSpeedBindable.Value = value;
}
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Purple;
/// <summary>
@ -49,12 +70,14 @@ namespace osu.Game.Beatmaps.ControlPoints
=> !OmitFirstBarLine
&& existing is EffectControlPoint existingEffect
&& KiaiMode == existingEffect.KiaiMode
&& OmitFirstBarLine == existingEffect.OmitFirstBarLine;
&& OmitFirstBarLine == existingEffect.OmitFirstBarLine
&& ScrollSpeed == existingEffect.ScrollSpeed;
public override void CopyFrom(ControlPoint other)
{
KiaiMode = ((EffectControlPoint)other).KiaiMode;
OmitFirstBarLine = ((EffectControlPoint)other).OmitFirstBarLine;
ScrollSpeed = ((EffectControlPoint)other).ScrollSpeed;
base.CopyFrom(other);
}

View File

@ -8,6 +8,9 @@ using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints
{
/// <remarks>
/// Note that going forward, this control point type should always be assigned directly to HitObjects.
/// </remarks>
public class SampleControlPoint : ControlPoint
{
public const string DEFAULT_BANK = "normal";

View File

@ -384,14 +384,21 @@ namespace osu.Game.Beatmaps.Formats
addControlPoint(time, new LegacyDifficultyControlPoint(beatLength)
#pragma warning restore 618
{
SpeedMultiplier = speedMultiplier,
SliderVelocity = speedMultiplier,
}, timingChange);
addControlPoint(time, new EffectControlPoint
var effectPoint = new EffectControlPoint
{
KiaiMode = kiaiMode,
OmitFirstBarLine = omitFirstBarSignature,
}, timingChange);
};
bool isOsuRuleset = beatmap.BeatmapInfo.RulesetID == 0;
// scrolling rulesets use effect points rather than difficulty points for scroll speed adjustments.
if (!isOsuRuleset)
effectPoint.ScrollSpeed = speedMultiplier;
addControlPoint(time, effectPoint, timingChange);
addControlPoint(time, new LegacySampleControlPoint
{

View File

@ -170,33 +170,30 @@ namespace osu.Game.Beatmaps.Formats
if (beatmap.ControlPointInfo.Groups.Count == 0)
return;
var legacyControlPoints = new LegacyControlPointInfo();
foreach (var point in beatmap.ControlPointInfo.AllControlPoints)
legacyControlPoints.Add(point.Time, point.DeepClone());
writer.WriteLine("[TimingPoints]");
if (!(beatmap.ControlPointInfo is LegacyControlPointInfo))
SampleControlPoint lastRelevantSamplePoint = null;
DifficultyControlPoint lastRelevantDifficultyPoint = null;
bool isOsuRuleset = beatmap.BeatmapInfo.RulesetID == 0;
// iterate over hitobjects and pull out all required sample and difficulty changes
extractDifficultyControlPoints(beatmap.HitObjects);
extractSampleControlPoints(beatmap.HitObjects);
// handle scroll speed, which is stored as "slider velocity" in legacy formats.
// this is relevant for scrolling ruleset beatmaps.
if (!isOsuRuleset)
{
var legacyControlPoints = new LegacyControlPointInfo();
foreach (var point in beatmap.ControlPointInfo.AllControlPoints)
legacyControlPoints.Add(point.Time, point.DeepClone());
beatmap.ControlPointInfo = legacyControlPoints;
SampleControlPoint lastRelevantSamplePoint = null;
// iterate over hitobjects and pull out all required sample changes
foreach (var h in beatmap.HitObjects)
{
var hSamplePoint = h.SampleControlPoint;
if (!hSamplePoint.IsRedundant(lastRelevantSamplePoint))
{
legacyControlPoints.Add(hSamplePoint.Time, hSamplePoint);
lastRelevantSamplePoint = hSamplePoint;
}
}
foreach (var point in legacyControlPoints.EffectPoints)
legacyControlPoints.Add(point.Time, new DifficultyControlPoint { SliderVelocity = point.ScrollSpeed });
}
foreach (var group in beatmap.ControlPointInfo.Groups)
foreach (var group in legacyControlPoints.Groups)
{
var groupTimingPoint = group.ControlPoints.OfType<TimingControlPoint>().FirstOrDefault();
@ -209,16 +206,16 @@ namespace osu.Game.Beatmaps.Formats
}
// Output any remaining effects as secondary non-timing control point.
var difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(group.Time);
var difficultyPoint = legacyControlPoints.DifficultyPointAt(group.Time);
writer.Write(FormattableString.Invariant($"{group.Time},"));
writer.Write(FormattableString.Invariant($"{-100 / difficultyPoint.SpeedMultiplier},"));
writer.Write(FormattableString.Invariant($"{-100 / difficultyPoint.SliderVelocity},"));
outputControlPointAt(group.Time, false);
}
void outputControlPointAt(double time, bool isTimingPoint)
{
var samplePoint = ((LegacyControlPointInfo)beatmap.ControlPointInfo).SamplePointAt(time);
var effectPoint = beatmap.ControlPointInfo.EffectPointAt(time);
var samplePoint = legacyControlPoints.SamplePointAt(time);
var effectPoint = legacyControlPoints.EffectPointAt(time);
// Apply the control point to a hit sample to uncover legacy properties (e.g. suffix)
HitSampleInfo tempHitSample = samplePoint.ApplyTo(new ConvertHitObjectParser.LegacyHitSampleInfo(string.Empty));
@ -230,7 +227,7 @@ namespace osu.Game.Beatmaps.Formats
if (effectPoint.OmitFirstBarLine)
effectFlags |= LegacyEffectFlags.OmitFirstBarLine;
writer.Write(FormattableString.Invariant($"{(int)beatmap.ControlPointInfo.TimingPointAt(time).TimeSignature},"));
writer.Write(FormattableString.Invariant($"{(int)legacyControlPoints.TimingPointAt(time).TimeSignature},"));
writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},"));
writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},"));
writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},"));
@ -238,6 +235,55 @@ namespace osu.Game.Beatmaps.Formats
writer.Write(FormattableString.Invariant($"{(int)effectFlags}"));
writer.WriteLine();
}
IEnumerable<DifficultyControlPoint> collectDifficultyControlPoints(IEnumerable<HitObject> hitObjects)
{
if (!isOsuRuleset)
yield break;
foreach (var hitObject in hitObjects)
{
yield return hitObject.DifficultyControlPoint;
foreach (var nested in collectDifficultyControlPoints(hitObject.NestedHitObjects))
yield return nested;
}
}
void extractDifficultyControlPoints(IEnumerable<HitObject> hitObjects)
{
foreach (var hDifficultyPoint in collectDifficultyControlPoints(hitObjects).OrderBy(dp => dp.Time))
{
if (!hDifficultyPoint.IsRedundant(lastRelevantDifficultyPoint))
{
legacyControlPoints.Add(hDifficultyPoint.Time, hDifficultyPoint);
lastRelevantDifficultyPoint = hDifficultyPoint;
}
}
}
IEnumerable<SampleControlPoint> collectSampleControlPoints(IEnumerable<HitObject> hitObjects)
{
foreach (var hitObject in hitObjects)
{
yield return hitObject.SampleControlPoint;
foreach (var nested in collectSampleControlPoints(hitObject.NestedHitObjects))
yield return nested;
}
}
void extractSampleControlPoints(IEnumerable<HitObject> hitObject)
{
foreach (var hSamplePoint in collectSampleControlPoints(hitObject).OrderBy(sp => sp.Time))
{
if (!hSamplePoint.IsRedundant(lastRelevantSamplePoint))
{
legacyControlPoints.Add(hSamplePoint.Time, hSamplePoint);
lastRelevantSamplePoint = hSamplePoint;
}
}
}
}
private void handleColours(TextWriter writer)

View File

@ -181,7 +181,7 @@ namespace osu.Game.Beatmaps.Formats
public LegacyDifficultyControlPoint()
{
SpeedMultiplierBindable.Precision = double.Epsilon;
SliderVelocityBindable.Precision = double.Epsilon;
}
public override void CopyFrom(ControlPoint other)

View File

@ -1,9 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Lists;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Beatmaps.Legacy
@ -14,9 +15,9 @@ namespace osu.Game.Beatmaps.Legacy
/// All sound points.
/// </summary>
[JsonProperty]
public IBindableList<SampleControlPoint> SamplePoints => samplePoints;
public IReadOnlyList<SampleControlPoint> SamplePoints => samplePoints;
private readonly BindableList<SampleControlPoint> samplePoints = new BindableList<SampleControlPoint>();
private readonly SortedList<SampleControlPoint> samplePoints = new SortedList<SampleControlPoint>(Comparer<SampleControlPoint>.Default);
/// <summary>
/// Finds the sound control point that is active at <paramref name="time"/>.
@ -26,35 +27,76 @@ namespace osu.Game.Beatmaps.Legacy
[NotNull]
public SampleControlPoint SamplePointAt(double time) => BinarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT);
/// <summary>
/// All difficulty points.
/// </summary>
[JsonProperty]
public IReadOnlyList<DifficultyControlPoint> DifficultyPoints => difficultyPoints;
private readonly SortedList<DifficultyControlPoint> difficultyPoints = new SortedList<DifficultyControlPoint>(Comparer<DifficultyControlPoint>.Default);
/// <summary>
/// Finds the difficulty control point that is active at <paramref name="time"/>.
/// </summary>
/// <param name="time">The time to find the difficulty control point at.</param>
/// <returns>The difficulty control point.</returns>
[NotNull]
public DifficultyControlPoint DifficultyPointAt(double time) => BinarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT);
public override void Clear()
{
base.Clear();
samplePoints.Clear();
difficultyPoints.Clear();
}
protected override bool CheckAlreadyExisting(double time, ControlPoint newPoint)
{
if (newPoint is SampleControlPoint)
switch (newPoint)
{
var existing = BinarySearch(SamplePoints, time);
return newPoint.IsRedundant(existing);
}
case SampleControlPoint _:
// intentionally don't use SamplePointAt (we always need to consider the first sample point).
var existing = BinarySearch(SamplePoints, time);
return newPoint.IsRedundant(existing);
return base.CheckAlreadyExisting(time, newPoint);
case DifficultyControlPoint _:
return newPoint.IsRedundant(DifficultyPointAt(time));
default:
return base.CheckAlreadyExisting(time, newPoint);
}
}
protected override void GroupItemAdded(ControlPoint controlPoint)
{
if (controlPoint is SampleControlPoint typed)
samplePoints.Add(typed);
switch (controlPoint)
{
case SampleControlPoint typed:
samplePoints.Add(typed);
return;
base.GroupItemAdded(controlPoint);
case DifficultyControlPoint typed:
difficultyPoints.Add(typed);
return;
default:
base.GroupItemAdded(controlPoint);
break;
}
}
protected override void GroupItemRemoved(ControlPoint controlPoint)
{
if (controlPoint is SampleControlPoint typed)
samplePoints.Remove(typed);
switch (controlPoint)
{
case SampleControlPoint typed:
samplePoints.Remove(typed);
break;
case DifficultyControlPoint typed:
difficultyPoints.Remove(typed);
break;
}
base.GroupItemRemoved(controlPoint);
}

View File

@ -189,11 +189,14 @@ namespace osu.Game.Beatmaps
/// </summary>
public void CancelAsyncLoad()
{
loadCancellation?.Cancel();
loadCancellation = new CancellationTokenSource();
lock (beatmapFetchLock)
{
loadCancellation?.Cancel();
loadCancellation = new CancellationTokenSource();
if (beatmapLoadTask?.IsCompleted != true)
beatmapLoadTask = null;
if (beatmapLoadTask?.IsCompleted != true)
beatmapLoadTask = null;
}
}
private CancellationTokenSource createCancellationTokenSource(TimeSpan? timeout)
@ -205,19 +208,27 @@ namespace osu.Game.Beatmaps
return new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(10));
}
private Task<IBeatmap> loadBeatmapAsync() => beatmapLoadTask ??= Task.Factory.StartNew(() =>
private readonly object beatmapFetchLock = new object();
private Task<IBeatmap> loadBeatmapAsync()
{
// Todo: Handle cancellation during beatmap parsing
var b = GetBeatmap() ?? new Beatmap();
lock (beatmapFetchLock)
{
return beatmapLoadTask ??= Task.Factory.StartNew(() =>
{
// Todo: Handle cancellation during beatmap parsing
var b = GetBeatmap() ?? new Beatmap();
// The original beatmap version needs to be preserved as the database doesn't contain it
BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion;
// The original beatmap version needs to be preserved as the database doesn't contain it
BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion;
// Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc)
b.BeatmapInfo = BeatmapInfo;
// Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc)
b.BeatmapInfo = BeatmapInfo;
return b;
}, loadCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
return b;
}, loadCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
}
public override string ToString() => BeatmapInfo.ToString();

View File

@ -66,8 +66,12 @@ namespace osu.Game.Beatmaps
lock (workingCache)
{
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == info.ID);
if (working != null)
{
Logger.Log($"Invalidating working beatmap cache for {info}");
workingCache.Remove(working);
}
}
}
@ -86,6 +90,7 @@ namespace osu.Game.Beatmaps
lock (workingCache)
{
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID);
if (working != null)
return working;

View File

@ -10,7 +10,7 @@ namespace osu.Game.Configuration
[Description("Never repeat")]
RandomPermutation,
[Description("Random")]
[Description("True Random")]
Random
}
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Database
/// </summary>
/// <typeparam name="TModel">The model type.</typeparam>
/// <typeparam name="TFileModel">The associated file join type.</typeparam>
public abstract class ArchiveModelManager<TModel, TFileModel> : ICanAcceptFiles, IModelManager<TModel>, IModelFileManager<TModel, TFileModel>, IPostImports<TModel>
public abstract class ArchiveModelManager<TModel, TFileModel> : ICanAcceptFiles, IModelManager<TModel>, IModelFileManager<TModel, TFileModel>
where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete
where TFileModel : class, INamedFileInfo, new()
{
@ -116,7 +116,7 @@ namespace osu.Game.Database
/// <param name="paths">One or more archive locations on disk.</param>
public Task Import(params string[] paths)
{
var notification = new ProgressNotification { State = ProgressNotificationState.Active };
var notification = new ImportProgressNotification();
PostNotification?.Invoke(notification);
@ -125,7 +125,7 @@ namespace osu.Game.Database
public Task Import(params ImportTask[] tasks)
{
var notification = new ProgressNotification { State = ProgressNotificationState.Active };
var notification = new ImportProgressNotification();
PostNotification?.Invoke(notification);

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Models;
#nullable enable
namespace osu.Game.Database
{
/// <summary>
/// A model that contains a list of files it is responsible for.
/// </summary>
public interface IHasRealmFiles
{
IList<RealmNamedFileUsage> Files { get; }
string Hash { get; set; }
}
}

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