mirror of
https://github.com/osukey/osukey.git
synced 2025-08-03 14:46:38 +09:00
updated to latest version of velchange
This commit is contained in:
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1 +1,2 @@
|
||||
github: ppy
|
||||
custom: https://osu.ppy.sh/home/support
|
||||
|
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@ -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)
|
||||
|
1
.github/workflows/diffcalc.yml
vendored
1
.github/workflows/diffcalc.yml
vendored
@ -53,6 +53,7 @@ jobs:
|
||||
diffcalc:
|
||||
name: Run
|
||||
runs-on: self-hosted
|
||||
timeout-minutes: 1440
|
||||
if: needs.metadata.outputs.continue == 'yes'
|
||||
needs: metadata
|
||||
strategy:
|
||||
|
2
.github/workflows/report-nunit.yml
vendored
2
.github/workflows/report-nunit.yml
vendored
@ -30,3 +30,5 @@ jobs:
|
||||
name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
|
||||
path: "*.trx"
|
||||
reporter: dotnet-trx
|
||||
list-suites: 'failed'
|
||||
list-tests: 'failed'
|
||||
|
@ -51,11 +51,11 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.918.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.924.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. -->
|
||||
<PackageReference Include="Realm" Version="10.5.0" />
|
||||
<PackageReference Include="Realm" Version="10.6.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -140,10 +140,10 @@ namespace osu.Desktop
|
||||
switch (activity)
|
||||
{
|
||||
case UserActivity.InGame game:
|
||||
return game.Beatmap.ToString();
|
||||
return game.BeatmapInfo.ToString();
|
||||
|
||||
case UserActivity.Editing edit:
|
||||
return edit.Beatmap.ToString();
|
||||
return edit.BeatmapInfo.ToString();
|
||||
|
||||
case UserActivity.InLobby lobby:
|
||||
return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value;
|
||||
|
@ -74,7 +74,10 @@ namespace osu.Desktop
|
||||
|
||||
// we want to allow multiple instances to be started when in debug.
|
||||
if (!DebugUtils.IsDebugBuild)
|
||||
{
|
||||
Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (tournamentClient)
|
||||
|
@ -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());
|
||||
|
||||
|
@ -29,8 +29,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
|
||||
protected CatchSelectionBlueprintTestScene()
|
||||
{
|
||||
EditorBeatmap = new EditorBeatmap(new CatchBeatmap());
|
||||
EditorBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = 0;
|
||||
EditorBeatmap = new EditorBeatmap(new CatchBeatmap()) { Difficulty = { CircleSize = 0 } };
|
||||
EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint
|
||||
{
|
||||
BeatLength = 100
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
protected override void AddHitObject(DrawableHitObject hitObject)
|
||||
{
|
||||
// Create nested bananas (but positions are not randomized because beatmap processing is not done).
|
||||
hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), Beatmap.Value.BeatmapInfo.BaseDifficulty);
|
||||
hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
base.AddHitObject(hitObject);
|
||||
}
|
||||
|
@ -4,9 +4,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
@ -23,11 +23,12 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
|
||||
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
protected override IBeatmap GetPlayableBeatmap()
|
||||
{
|
||||
Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderTickRate = 5;
|
||||
Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity * 10;
|
||||
var playable = base.GetPlayableBeatmap();
|
||||
playable.Difficulty.SliderTickRate = 5;
|
||||
playable.Difficulty.SliderMultiplier = velocity * 10;
|
||||
return playable;
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
X = x,
|
||||
Path = sliderPath,
|
||||
};
|
||||
EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity;
|
||||
EditorBeatmap.Difficulty.SliderMultiplier = velocity;
|
||||
EditorBeatmap.Add(hitObject);
|
||||
EditorBeatmap.Update(hitObject);
|
||||
Assert.That(hitObject.Velocity, Is.EqualTo(velocity));
|
||||
|
@ -290,7 +290,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
public IEnumerable<CaughtObject> CaughtObjects => this.ChildrenOfType<CaughtObject>();
|
||||
|
||||
public TestCatcher(DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty)
|
||||
public TestCatcher(DroppedObjectContainer droppedObjectTarget, IBeatmapDifficultyInfo difficulty)
|
||||
: base(droppedObjectTarget, difficulty)
|
||||
{
|
||||
}
|
||||
@ -298,7 +298,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
public class TestKiaiFruit : Fruit
|
||||
{
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
private ScheduledDelegate addManyFruit;
|
||||
|
||||
private BeatmapDifficulty beatmapDifficulty;
|
||||
private IBeatmapDifficultyInfo beatmapDifficulty;
|
||||
|
||||
public TestSceneCatcherArea()
|
||||
{
|
||||
@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
private class TestCatcherArea : CatcherArea
|
||||
{
|
||||
public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
|
||||
public TestCatcherArea(IBeatmapDifficultyInfo beatmapDifficulty)
|
||||
{
|
||||
var droppedObjectContainer = new DroppedObjectContainer();
|
||||
Add(droppedObjectContainer);
|
||||
|
@ -211,7 +211,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
|
||||
palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
|
||||
|
||||
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2;
|
||||
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) / 2;
|
||||
|
||||
// Todo: This is wrong. osu!stable calculated hyperdashes using the full catcher size, excluding the margins.
|
||||
// This should theoretically cause impossible scenarios, but practically, likely due to the size of the playfield, it doesn't seem possible.
|
||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
return new CatchDifficultyAttributes { Mods = mods, Skills = skills };
|
||||
|
||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||
double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
return new CatchDifficultyAttributes
|
||||
{
|
||||
@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
// In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream.
|
||||
foreach (var hitObject in beatmap.HitObjects
|
||||
.SelectMany(obj => obj is JuiceStream stream ? stream.NestedHitObjects : new[] { obj })
|
||||
.SelectMany(obj => obj is JuiceStream stream ? stream.NestedHitObjects.AsEnumerable() : new[] { obj })
|
||||
.Cast<CatchHitObject>()
|
||||
.OrderBy(x => x.StartTime))
|
||||
{
|
||||
@ -69,10 +69,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
{
|
||||
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
|
||||
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) * 0.5f;
|
||||
|
||||
// For circle sizes above 5.5, reduce the catcher width further to simulate imperfect gameplay.
|
||||
halfCatcherWidth *= 1 - (Math.Max(0, beatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5.5f) * 0.0625f);
|
||||
halfCatcherWidth *= 1 - (Math.Max(0, beatmap.Difficulty.CircleSize - 5.5f) * 0.0625f);
|
||||
|
||||
return new Skill[]
|
||||
{
|
||||
|
@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
public class CatchEditorPlayfield : CatchPlayfield
|
||||
{
|
||||
// TODO fixme: the size of the catcher is not changed when circle size is changed in setup screen.
|
||||
public CatchEditorPlayfield(BeatmapDifficulty difficulty)
|
||||
public CatchEditorPlayfield(IBeatmapDifficultyInfo difficulty)
|
||||
: base(difficulty)
|
||||
{
|
||||
}
|
||||
|
@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
}
|
||||
|
||||
protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
|
||||
protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.Difficulty);
|
||||
}
|
||||
}
|
||||
|
@ -128,11 +128,11 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
/// </summary>
|
||||
public int RandomSeed => (int)StartTime;
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
|
||||
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
|
||||
|
||||
Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2;
|
||||
}
|
||||
|
@ -37,14 +37,13 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
/// </summary>
|
||||
public double SpanDuration => Duration / this.SpanCount();
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
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;
|
||||
|
@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
internal CatcherArea CatcherArea { get; private set; }
|
||||
|
||||
private readonly BeatmapDifficulty difficulty;
|
||||
private readonly IBeatmapDifficultyInfo difficulty;
|
||||
|
||||
public CatchPlayfield(BeatmapDifficulty difficulty)
|
||||
public CatchPlayfield(IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
this.difficulty = difficulty;
|
||||
}
|
||||
|
@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
private readonly DrawablePool<CaughtBanana> caughtBananaPool;
|
||||
private readonly DrawablePool<CaughtDroplet> caughtDropletPool;
|
||||
|
||||
public Catcher([NotNull] DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty = null)
|
||||
public Catcher([NotNull] DroppedObjectContainer droppedObjectTarget, IBeatmapDifficultyInfo difficulty = null)
|
||||
{
|
||||
this.droppedObjectTarget = droppedObjectTarget;
|
||||
|
||||
@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// <summary>
|
||||
/// Calculates the scale of the catcher based off the provided beatmap difficulty.
|
||||
/// </summary>
|
||||
private static Vector2 calculateScale(BeatmapDifficulty difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
|
||||
private static Vector2 calculateScale(IBeatmapDifficultyInfo difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the width of the area used for attempting catches in gameplay.
|
||||
@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// Calculates the width of the area used for attempting catches in gameplay.
|
||||
/// </summary>
|
||||
/// <param name="difficulty">The beatmap difficulty.</param>
|
||||
public static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty));
|
||||
public static float CalculateCatchWidth(IBeatmapDifficultyInfo difficulty) => CalculateCatchWidth(calculateScale(difficulty));
|
||||
|
||||
/// <summary>
|
||||
/// Determine if this catcher can catch a <see cref="CatchHitObject"/> in the current position.
|
||||
|
@ -27,14 +27,14 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
: base(ruleset, beatmap, mods)
|
||||
{
|
||||
Direction.Value = ScrollingDirection.Down;
|
||||
TimeRange.Value = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450);
|
||||
TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450);
|
||||
}
|
||||
|
||||
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
|
||||
|
||||
protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield);
|
||||
|
||||
protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
|
||||
protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.Difficulty);
|
||||
|
||||
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchPlayfieldAdjustmentContainer();
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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", () =>
|
||||
|
@ -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 } });
|
||||
|
||||
|
@ -42,8 +42,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
{
|
||||
IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
|
||||
|
||||
var roundedCircleSize = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
|
||||
var roundedOverallDifficulty = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||
var roundedCircleSize = Math.Round(beatmap.Difficulty.CircleSize);
|
||||
var roundedOverallDifficulty = Math.Round(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
if (IsForCurrentRuleset)
|
||||
{
|
||||
@ -71,9 +71,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
originalTargetColumns = TargetColumns;
|
||||
}
|
||||
|
||||
public static int GetColumnCountForNonConvert(BeatmapInfo beatmap)
|
||||
public static int GetColumnCountForNonConvert(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
var roundedCircleSize = Math.Round(beatmap.BaseDifficulty.CircleSize);
|
||||
var roundedCircleSize = Math.Round(beatmapInfo.BaseDifficulty.CircleSize);
|
||||
return (int)Math.Max(1, roundedCircleSize);
|
||||
}
|
||||
|
||||
@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
|
||||
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
|
||||
{
|
||||
BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty;
|
||||
IBeatmapDifficultyInfo difficulty = original.Difficulty;
|
||||
|
||||
int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
|
||||
Random = new FastRandom(seed);
|
||||
|
@ -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,13 +55,13 @@ 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);
|
||||
|
||||
// This matches stable's calculation.
|
||||
EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier);
|
||||
EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.Difficulty.SliderMultiplier);
|
||||
|
||||
SegmentDuration = (EndTime - StartTime) / SpanCount;
|
||||
}
|
||||
|
@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
if (drainTime == 0)
|
||||
drainTime = 10000;
|
||||
|
||||
BeatmapDifficulty difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty;
|
||||
IBeatmapDifficultyInfo difficulty = OriginalBeatmap.Difficulty;
|
||||
conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
|
||||
conversionDifficulty = Math.Min(conversionDifficulty.Value, 12);
|
||||
|
||||
@ -149,7 +149,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
{
|
||||
lowerBound ??= RandomStart;
|
||||
upperBound ??= TotalColumns;
|
||||
nextColumn ??= (_ => GetRandomColumn(lowerBound, upperBound));
|
||||
nextColumn ??= _ => GetRandomColumn(lowerBound, upperBound);
|
||||
|
||||
// Check for the initial column
|
||||
if (isValid(initialColumn))
|
||||
@ -176,7 +176,19 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
|
||||
return initialColumn;
|
||||
|
||||
bool isValid(int column) => validation?.Invoke(column) != false && !patterns.Any(p => p.ColumnHasObject(column));
|
||||
bool isValid(int column)
|
||||
{
|
||||
if (validation?.Invoke(column) == false)
|
||||
return false;
|
||||
|
||||
foreach (var p in patterns)
|
||||
{
|
||||
if (p.ColumnHasObject(column))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -12,46 +12,68 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
|
||||
/// </summary>
|
||||
internal class Pattern
|
||||
{
|
||||
private readonly List<ManiaHitObject> hitObjects = new List<ManiaHitObject>();
|
||||
private List<ManiaHitObject> hitObjects;
|
||||
private HashSet<int> containedColumns;
|
||||
|
||||
/// <summary>
|
||||
/// All the hit objects contained in this pattern.
|
||||
/// </summary>
|
||||
public IEnumerable<ManiaHitObject> HitObjects => hitObjects;
|
||||
public IEnumerable<ManiaHitObject> HitObjects => hitObjects ?? Enumerable.Empty<ManiaHitObject>();
|
||||
|
||||
/// <summary>
|
||||
/// Check whether a column of this patterns contains a hit object.
|
||||
/// </summary>
|
||||
/// <param name="column">The column index.</param>
|
||||
/// <returns>Whether the column with index <paramref name="column"/> contains a hit object.</returns>
|
||||
public bool ColumnHasObject(int column) => hitObjects.Exists(h => h.Column == column);
|
||||
public bool ColumnHasObject(int column) => containedColumns?.Contains(column) == true;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of columns taken up by hit objects in this pattern.
|
||||
/// </summary>
|
||||
public int ColumnWithObjects => HitObjects.GroupBy(h => h.Column).Count();
|
||||
public int ColumnWithObjects => containedColumns?.Count ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a hit object to this pattern.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The hit object to add.</param>
|
||||
public void Add(ManiaHitObject hitObject) => hitObjects.Add(hitObject);
|
||||
public void Add(ManiaHitObject hitObject)
|
||||
{
|
||||
prepareStorage();
|
||||
|
||||
hitObjects.Add(hitObject);
|
||||
containedColumns.Add(hitObject.Column);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies hit object from another pattern to this one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other pattern.</param>
|
||||
public void Add(Pattern other) => hitObjects.AddRange(other.HitObjects);
|
||||
public void Add(Pattern other)
|
||||
{
|
||||
prepareStorage();
|
||||
|
||||
if (other.hitObjects != null)
|
||||
{
|
||||
hitObjects.AddRange(other.hitObjects);
|
||||
|
||||
foreach (var h in other.hitObjects)
|
||||
containedColumns.Add(h.Column);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears this pattern, removing all hit objects.
|
||||
/// </summary>
|
||||
public void Clear() => hitObjects.Clear();
|
||||
public void Clear()
|
||||
{
|
||||
hitObjects?.Clear();
|
||||
containedColumns?.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a hit object from this pattern.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The hit object to remove.</param>
|
||||
public bool Remove(ManiaHitObject hitObject) => hitObjects.Remove(hitObject);
|
||||
private void prepareStorage()
|
||||
{
|
||||
hitObjects ??= new List<ManiaHitObject>();
|
||||
containedColumns ??= new HashSet<int>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,12 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
|
||||
{
|
||||
new TrackedSetting<double>(ManiaRulesetSetting.ScrollTime,
|
||||
v => new SettingDescription(v, "Scroll Speed", $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / v)} ({v}ms)"))
|
||||
scrollTime => new SettingDescription(
|
||||
rawValue: scrollTime,
|
||||
name: "Scroll Speed",
|
||||
value: $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)} ({scrollTime}ms)"
|
||||
)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -41,15 +41,14 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
return new ManiaDifficultyAttributes { Mods = mods, Skills = skills };
|
||||
|
||||
HitWindows hitWindows = new ManiaHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
return new ManiaDifficultyAttributes
|
||||
{
|
||||
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),
|
||||
ScoreMultiplier = getScoreMultiplier(beatmap, mods),
|
||||
GreatHitWindow = Math.Ceiling(getHitWindow300(mods) / clockRate),
|
||||
ScoreMultiplier = getScoreMultiplier(mods),
|
||||
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
|
||||
Skills = skills
|
||||
};
|
||||
@ -70,7 +69,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
|
||||
{
|
||||
new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns)
|
||||
new Strain(mods, ((ManiaBeatmap)Beatmap).TotalColumns)
|
||||
};
|
||||
|
||||
protected override Mod[] DifficultyAdjustmentMods
|
||||
@ -138,7 +137,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
}
|
||||
}
|
||||
|
||||
private double getScoreMultiplier(IBeatmap beatmap, Mod[] mods)
|
||||
private double getScoreMultiplier(Mod[] mods)
|
||||
{
|
||||
double scoreMultiplier = 1;
|
||||
|
||||
@ -154,7 +153,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
}
|
||||
}
|
||||
|
||||
var maniaBeatmap = (ManiaBeatmap)beatmap;
|
||||
var maniaBeatmap = (ManiaBeatmap)Beatmap;
|
||||
int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns;
|
||||
|
||||
if (diff > 0)
|
||||
|
@ -13,9 +13,9 @@ namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
private FilterCriteria.OptionalRange<float> keys;
|
||||
|
||||
public bool Matches(BeatmapInfo beatmap)
|
||||
public bool Matches(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
return !keys.HasFilter || (beatmap.RulesetID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmap)));
|
||||
return !keys.HasFilter || (beatmapInfo.RulesetID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo)));
|
||||
}
|
||||
|
||||
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
|
||||
|
@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
/// </summary>
|
||||
private double tickSpacing = 50;
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
|
@ -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:
|
||||
|
@ -90,11 +90,11 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
// Mania doesn't care about global velocity
|
||||
p.Velocity = 1;
|
||||
p.BaseBeatLength *= Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier;
|
||||
p.BaseBeatLength *= Beatmap.Difficulty.SliderMultiplier;
|
||||
|
||||
// For non-mania beatmap, speed changes should only happen through timing points
|
||||
if (!isForCurrentRuleset)
|
||||
p.DifficultyPoint = new DifficultyControlPoint();
|
||||
p.EffectPoint = new EffectControlPoint();
|
||||
}
|
||||
|
||||
BarLines.ForEach(Playfield.Add);
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
|
||||
public class CheckTooShortSpinnersTest
|
||||
{
|
||||
private CheckTooShortSpinners check;
|
||||
private BeatmapDifficulty difficulty;
|
||||
private IBeatmapDifficultyInfo difficulty;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
@ -81,12 +81,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
|
||||
assertTooShort(new List<HitObject> { spinnerHighOd }, difficultyHighOd);
|
||||
}
|
||||
|
||||
private void assertOk(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
|
||||
private void assertOk(List<HitObject> hitObjects, IBeatmapDifficultyInfo beatmapDifficulty)
|
||||
{
|
||||
Assert.That(check.Run(getContext(hitObjects, beatmapDifficulty)), Is.Empty);
|
||||
}
|
||||
|
||||
private void assertVeryShort(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
|
||||
private void assertVeryShort(List<HitObject> hitObjects, IBeatmapDifficultyInfo beatmapDifficulty)
|
||||
{
|
||||
var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList();
|
||||
|
||||
@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
|
||||
Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateVeryShort);
|
||||
}
|
||||
|
||||
private void assertTooShort(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
|
||||
private void assertTooShort(List<HitObject> hitObjects, IBeatmapDifficultyInfo beatmapDifficulty)
|
||||
{
|
||||
var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList();
|
||||
|
||||
@ -102,12 +102,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
|
||||
Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateTooShort);
|
||||
}
|
||||
|
||||
private BeatmapVerifierContext getContext(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
|
||||
private BeatmapVerifierContext getContext(List<HitObject> hitObjects, IBeatmapDifficultyInfo beatmapDifficulty)
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = hitObjects,
|
||||
BeatmapInfo = new BeatmapInfo { BaseDifficulty = beatmapDifficulty }
|
||||
BeatmapInfo = new BeatmapInfo { BaseDifficulty = new BeatmapDifficulty(beatmapDifficulty) }
|
||||
};
|
||||
|
||||
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||
|
@ -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;
|
||||
@ -45,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
|
||||
editorBeatmap.Difficulty.SliderMultiplier = 1;
|
||||
editorBeatmap.ControlPointInfo.Clear();
|
||||
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -4,13 +4,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Play;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
@ -122,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
private bool checkSomeHit() => Player.ScoreProcessor.JudgedHits >= 4;
|
||||
|
||||
private bool objectWithIncreasedVisibilityHasIndex(int index)
|
||||
=> Player.Mods.Value.OfType<TestOsuModHidden>().Single().FirstObject == Player.ChildrenOfType<GameplayBeatmap>().Single().HitObjects[index];
|
||||
=> Player.GameplayState.Mods.OfType<TestOsuModHidden>().Single().FirstObject == Player.GameplayState.Beatmap.HitObjects[index];
|
||||
|
||||
private class TestOsuModHidden : OsuModHidden
|
||||
{
|
||||
|
@ -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());
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 241 KiB After Width: | Height: | Size: 26 KiB |
@ -0,0 +1,3 @@
|
||||
[General]
|
||||
Version: latest
|
||||
HitCircleOverlayAboveNumber: 0
|
@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
Position = new Vector2(100, 300),
|
||||
},
|
||||
accuracyHeatmap = new TestAccuracyHeatmap(new ScoreInfo { Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo })
|
||||
accuracyHeatmap = new TestAccuracyHeatmap(new ScoreInfo { BeatmapInfo = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo })
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
|
@ -17,6 +17,7 @@ using osu.Framework.Testing.Input;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Skinning;
|
||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||
using osu.Game.Screens.Play;
|
||||
@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
public class TestSceneGameplayCursor : OsuSkinnableTestScene
|
||||
{
|
||||
[Cached]
|
||||
private GameplayBeatmap gameplayBeatmap;
|
||||
private GameplayState gameplayState;
|
||||
|
||||
private OsuCursorContainer lastContainer;
|
||||
|
||||
@ -40,7 +41,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
public TestSceneGameplayCursor()
|
||||
{
|
||||
gameplayBeatmap = new GameplayBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
|
||||
var ruleset = new OsuRuleset();
|
||||
gameplayState = new GameplayState(CreateBeatmap(ruleset.RulesetInfo), ruleset, Array.Empty<Mod>());
|
||||
|
||||
AddStep("change background colour", () =>
|
||||
{
|
||||
@ -57,8 +59,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddSliderStep("circle size", 0f, 10f, 0f, val =>
|
||||
{
|
||||
config.SetValue(OsuSetting.AutoCursorSize, true);
|
||||
gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = val;
|
||||
Scheduler.AddOnce(() => loadContent(false));
|
||||
gameplayState.Beatmap.Difficulty.CircleSize = val;
|
||||
Scheduler.AddOnce(loadContent);
|
||||
});
|
||||
|
||||
AddStep("test cursor container", () => loadContent(false));
|
||||
@ -73,10 +75,10 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
public void TestSizing(int circleSize, float userScale)
|
||||
{
|
||||
AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale));
|
||||
AddStep($"adjust cs to {circleSize}", () => gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize);
|
||||
AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.Difficulty.CircleSize = circleSize);
|
||||
AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true));
|
||||
|
||||
AddStep("load content", () => loadContent());
|
||||
AddStep("load content", loadContent);
|
||||
|
||||
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale);
|
||||
|
||||
@ -96,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(_ =>
|
||||
{
|
||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
};
|
||||
|
||||
var hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
};
|
||||
|
||||
var hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
|
@ -400,15 +400,13 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
|
||||
{
|
||||
HitObjects = hitObjects,
|
||||
Difficulty = new BeatmapDifficulty { SliderTickRate = 3 },
|
||||
BeatmapInfo =
|
||||
{
|
||||
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
|
||||
Ruleset = new OsuRuleset().RulesetInfo
|
||||
},
|
||||
});
|
||||
|
||||
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();
|
||||
@ -452,7 +452,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
private class TestSpinner : Spinner
|
||||
{
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
SpinsRequired = 1;
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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 += _ =>
|
||||
|
@ -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
|
||||
},
|
||||
}
|
||||
};
|
||||
|
@ -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();
|
||||
@ -412,7 +412,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
private class TestSpinner : Spinner
|
||||
{
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
SpinsRequired = 1;
|
||||
|
@ -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:
|
||||
|
@ -12,7 +12,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
public double FlashlightRating { get; set; }
|
||||
public double ApproachRate { get; set; }
|
||||
public double OverallDifficulty { get; set; }
|
||||
public double DrainRate { get; set; }
|
||||
public int HitCircleCount { get; set; }
|
||||
public int SliderCount { get; set; }
|
||||
public int SpinnerCount { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
|
||||
double flashlightRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
|
||||
|
||||
if (mods.Any(h => h is OsuModRelax))
|
||||
speedRating = 0.0;
|
||||
|
||||
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
|
||||
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
|
||||
double baseFlashlightPerformance = 0.0;
|
||||
@ -53,13 +56,15 @@ 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)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
double drainRate = beatmap.Difficulty.DrainRate;
|
||||
|
||||
int maxCombo = beatmap.HitObjects.Count;
|
||||
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
|
||||
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
|
||||
|
||||
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
|
||||
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
||||
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
|
||||
|
||||
return new OsuDifficultyAttributes
|
||||
@ -71,8 +76,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
FlashlightRating = flashlightRating,
|
||||
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
||||
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
||||
DrainRate = drainRate,
|
||||
MaxCombo = maxCombo,
|
||||
HitCircleCount = hitCirclesCount,
|
||||
SliderCount = sliderCount,
|
||||
SpinnerCount = spinnerCount,
|
||||
Skills = skills
|
||||
};
|
||||
@ -95,10 +102,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
{
|
||||
HitWindows hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
// Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future
|
||||
hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate;
|
||||
hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
|
||||
return new Skill[]
|
||||
{
|
||||
|
@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
public class OsuDifficultyHitObject : DifficultyHitObject
|
||||
{
|
||||
private const int normalized_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
|
||||
private const int min_delta_time = 25;
|
||||
|
||||
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
|
||||
|
||||
@ -22,9 +23,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
public double JumpDistance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalized Vector from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
|
||||
/// Minimum distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
|
||||
/// </summary>
|
||||
public Vector2 JumpVector { get; private set; }
|
||||
public double MovementDistance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalized distance between the start and end position of the previous <see cref="OsuDifficultyHitObject"/>.
|
||||
@ -38,7 +39,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
public double? Angle { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 50ms.
|
||||
/// Milliseconds elapsed since the end time of the Previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
|
||||
/// </summary>
|
||||
public double MovementTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Milliseconds elapsed since from the start time of the Previous <see cref="OsuDifficultyHitObject"/> to the end time of the same Previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
|
||||
/// </summary>
|
||||
public double TravelTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
|
||||
/// </summary>
|
||||
public readonly double StrainTime;
|
||||
|
||||
@ -51,14 +62,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
this.lastLastObject = (OsuHitObject)lastLastObject;
|
||||
this.lastObject = (OsuHitObject)lastObject;
|
||||
|
||||
setDistances();
|
||||
|
||||
// Capped to 25ms to prevent difficulty calculation breaking from simulatenous objects.
|
||||
StrainTime = Math.Max(DeltaTime, 25);
|
||||
StrainTime = Math.Max(DeltaTime, min_delta_time);
|
||||
|
||||
setDistances(clockRate);
|
||||
}
|
||||
|
||||
private void setDistances()
|
||||
private void setDistances(double clockRate)
|
||||
{
|
||||
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
|
||||
if (BaseObject is Spinner || lastObject is Spinner)
|
||||
return;
|
||||
|
||||
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||
float scalingFactor = normalized_radius / (float)BaseObject.Radius;
|
||||
|
||||
@ -71,19 +86,41 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
if (lastObject is Slider lastSlider)
|
||||
{
|
||||
computeSliderCursorPosition(lastSlider);
|
||||
TravelDistance = lastSlider.LazyTravelDistance * scalingFactor;
|
||||
TravelDistance = 0;
|
||||
TravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time);
|
||||
MovementTime = Math.Max(StrainTime - TravelTime, min_delta_time);
|
||||
MovementDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor;
|
||||
|
||||
int repeatCount = 0;
|
||||
|
||||
for (int i = 1; i < lastSlider.NestedHitObjects.Count; i++)
|
||||
{
|
||||
if ((OsuHitObject)lastSlider.NestedHitObjects[i] is SliderRepeat)
|
||||
repeatCount++;
|
||||
|
||||
Vector2 currSlider = Vector2.Subtract(((OsuHitObject)lastSlider.NestedHitObjects[i]).StackedPosition, ((OsuHitObject)lastSlider.NestedHitObjects[i - 1]).StackedPosition);
|
||||
|
||||
if ((OsuHitObject)lastSlider.NestedHitObjects[i] is SliderEndCircle || (OsuHitObject)lastSlider.NestedHitObjects[i] is SliderRepeat)
|
||||
TravelDistance += Math.Max(0, currSlider.Length * scalingFactor - 100);
|
||||
else
|
||||
TravelDistance += Vector2.Subtract(((OsuHitObject)lastSlider.NestedHitObjects[i]).StackedPosition, ((OsuHitObject)lastSlider.NestedHitObjects[i - 1]).StackedPosition).Length * scalingFactor;
|
||||
|
||||
if ((OsuHitObject)lastSlider.NestedHitObjects[i] is SliderTick && i != lastSlider.NestedHitObjects.Count - 1) // Check for tick && not last object is necessary for 2007 bugged sliders.
|
||||
{
|
||||
Vector2 nextSlider = Vector2.Subtract(((OsuHitObject)lastSlider.NestedHitObjects[i + 1]).StackedPosition, ((OsuHitObject)lastSlider.NestedHitObjects[i]).StackedPosition);
|
||||
TravelDistance += 2 * Vector2.Subtract(nextSlider, currSlider).Length * scalingFactor;
|
||||
}
|
||||
}
|
||||
|
||||
TravelDistance *= Math.Max(0, Math.Min(TravelTime, lastSlider.SpanDuration - 50)) / lastSlider.SpanDuration;
|
||||
}
|
||||
|
||||
Vector2 lastCursorPosition = getEndCursorPosition(lastObject);
|
||||
|
||||
// Don't need to jump to reach spinners
|
||||
if (!(BaseObject is Spinner))
|
||||
{
|
||||
JumpVector = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor);
|
||||
JumpDistance = JumpVector.Length;
|
||||
}
|
||||
JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
|
||||
MovementDistance = Math.Min(JumpDistance, MovementDistance);
|
||||
|
||||
if (lastLastObject != null)
|
||||
if (lastLastObject != null && !(lastLastObject is Spinner))
|
||||
{
|
||||
Vector2 lastLastCursorPosition = getEndCursorPosition(lastLastObject);
|
||||
|
||||
@ -104,7 +141,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
|
||||
slider.LazyEndPosition = slider.StackedPosition;
|
||||
|
||||
float approxFollowCircleRadius = (float)(slider.Radius * 3);
|
||||
float followCircleRadius = (float)(slider.Radius * 2.4);
|
||||
var computeVertex = new Action<double>(t =>
|
||||
{
|
||||
double progress = (t - slider.StartTime) / slider.SpanDuration;
|
||||
@ -117,11 +154,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
var diff = slider.StackedPosition + slider.Path.PositionAt(progress) - slider.LazyEndPosition.Value;
|
||||
float dist = diff.Length;
|
||||
|
||||
if (dist > approxFollowCircleRadius)
|
||||
slider.LazyTravelTime = t - slider.StartTime;
|
||||
|
||||
if (dist > followCircleRadius)
|
||||
{
|
||||
// The cursor would be outside the follow circle, we need to move it
|
||||
diff.Normalize(); // Obtain direction of diff
|
||||
dist -= approxFollowCircleRadius;
|
||||
dist -= followCircleRadius;
|
||||
slider.LazyEndPosition += diff * dist;
|
||||
slider.LazyTravelDistance += dist;
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Framework.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
@ -21,102 +20,122 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
}
|
||||
|
||||
private int count = 1;
|
||||
|
||||
protected override int HistoryLength => 2;
|
||||
|
||||
protected override double SkillMultiplier => 24.75;
|
||||
protected override double StrainDecayBase => 0.15;
|
||||
private const double wide_angle_multiplier = 1.5;
|
||||
private const double acute_angle_multiplier = 1.5;
|
||||
private const double slider_multiplier = 1.75;
|
||||
private const double vel_change_multiplier = 0.75;
|
||||
|
||||
private const double wide_angle_multiplier = 1.0;
|
||||
private const double acute_angle_multiplier = 1.0;
|
||||
private const double rhythm_variance_multiplier = 1.0;
|
||||
private const double vel_change_multiplier = 6.5;
|
||||
private const double slider_multiplier = 6.5;
|
||||
private const double slider_jump_multiplier = 0.875;
|
||||
private double currentStrain = 1;
|
||||
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
private double skillMultiplier => 23.25;
|
||||
private double strainDecayBase => 0.15;
|
||||
|
||||
private double strainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
if (current.BaseObject is Spinner || Previous.Count <= 1)
|
||||
if (current.BaseObject is Spinner || Previous.Count <= 1 || Previous[0].BaseObject is Spinner)
|
||||
return 0;
|
||||
|
||||
count++;
|
||||
|
||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
||||
var osuPrevObj = (OsuDifficultyHitObject)Previous[0];
|
||||
var osuLastObj = (OsuDifficultyHitObject)Previous[1];
|
||||
|
||||
var currVector = Vector2.Divide(osuCurrObj.JumpVector, (float)osuCurrObj.StrainTime);
|
||||
var prevVector = Vector2.Divide(osuPrevObj.JumpVector, (float)osuPrevObj.StrainTime);
|
||||
double currVelocity = osuCurrObj.JumpDistance / osuCurrObj.StrainTime; // Start with the base distance / time
|
||||
|
||||
// Start with regular velocity.
|
||||
double aimStrain = currVector.Length;
|
||||
|
||||
if (Precision.AlmostEquals(osuCurrObj.StrainTime, osuPrevObj.StrainTime, 10)) // Rhythms are the same.
|
||||
if (osuPrevObj.BaseObject is Slider) // If object is a slider
|
||||
{
|
||||
if (osuCurrObj.Angle != null)
|
||||
double movementVelocity = osuCurrObj.MovementDistance / osuCurrObj.MovementTime; // calculate the movement velocity from slider end to next note
|
||||
double travelVelocity = osuCurrObj.TravelDistance / osuCurrObj.TravelTime; // calculate the slider velocity from slider head to lazy end.
|
||||
|
||||
currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity.
|
||||
}
|
||||
|
||||
double prevVelocity = osuPrevObj.JumpDistance / osuPrevObj.StrainTime; // do the same for the previous velocity.
|
||||
|
||||
if (osuLastObj.BaseObject is Slider)
|
||||
{
|
||||
double movementVelocity = osuPrevObj.MovementDistance / osuPrevObj.MovementTime;
|
||||
double travelVelocity = osuPrevObj.TravelDistance / osuPrevObj.TravelTime;
|
||||
|
||||
prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity);
|
||||
}
|
||||
|
||||
double angleBonus = 0;
|
||||
double sliderBonus = 0;
|
||||
double velChangeBonus = 0;
|
||||
|
||||
double aimStrain = currVelocity; // Start strain with regular velocity.
|
||||
|
||||
if (Precision.AlmostEquals(osuCurrObj.StrainTime, osuPrevObj.StrainTime, 10)) // If rhythms are the same.
|
||||
{
|
||||
if (osuCurrObj.Angle != null && osuPrevObj.Angle != null)
|
||||
{
|
||||
double angle = osuCurrObj.Angle.Value;
|
||||
double currAngle = osuCurrObj.Angle.Value;
|
||||
double prevAngle = osuPrevObj.Angle.Value;
|
||||
|
||||
// Rewarding angles, take the smaller velocity as base.
|
||||
double angleBonus = Math.Min(currVector.Length, prevVector.Length);
|
||||
angleBonus = Math.Min(currVelocity, prevVelocity);
|
||||
|
||||
double wideAngleBonus = calcWideAngleBonus(angle);
|
||||
double acuteAngleBonus = calcAcuteAngleBonus(angle);
|
||||
double wideAngleBonus = calcWideAngleBonus(currAngle);
|
||||
double acuteAngleBonus = calcAcuteAngleBonus(currAngle);
|
||||
|
||||
if (osuCurrObj.StrainTime > 100)
|
||||
if (osuCurrObj.StrainTime > 100) // Only buff deltaTime exceeding 300 bpm 1/2.
|
||||
acuteAngleBonus = 0;
|
||||
else
|
||||
{
|
||||
acuteAngleBonus *= Math.Min(2, Math.Pow((100 - osuCurrObj.StrainTime) / 15, 1.5));
|
||||
wideAngleBonus *= Math.Pow(osuCurrObj.StrainTime / 100, 6);
|
||||
}
|
||||
acuteAngleBonus *= calcAcuteAngleBonus(prevAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
|
||||
* Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
|
||||
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4
|
||||
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Min(100, osuCurrObj.JumpDistance) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter).
|
||||
|
||||
if (acuteAngleBonus > wideAngleBonus)
|
||||
angleBonus = Math.Min(angleBonus, 150 / osuCurrObj.StrainTime) * Math.Min(1, Math.Pow(Math.Min(osuCurrObj.JumpDistance, osuPrevObj.JumpDistance) / 150, 2));
|
||||
wideAngleBonus *= angleBonus * (1 - Math.Pow(calcWideAngleBonus(prevAngle), 3)); // Penalize wide angles if they're repeated, reducing the penalty as the prevAngle gets more acute.
|
||||
|
||||
angleBonus *= Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier);
|
||||
|
||||
// add in angle velocity.
|
||||
aimStrain += angleBonus;
|
||||
|
||||
if (prevVector.Length > currVector.Length)
|
||||
{
|
||||
double velChangeBonus = Math.Max(0, Math.Sqrt((prevVector.Length - currVector.Length) * currVector.Length) - currVector.Length) * Math.Min(1, osuCurrObj.JumpDistance / 100);
|
||||
|
||||
aimStrain += velChangeBonus * Math.Sqrt(100 / osuCurrObj.StrainTime) * vel_change_multiplier;
|
||||
}
|
||||
angleBonus = acuteAngleBonus * acute_angle_multiplier + wideAngleBonus * wide_angle_multiplier; // add the anglebuffs together.
|
||||
}
|
||||
}
|
||||
else // There is a rhythm change
|
||||
|
||||
if (Math.Max(prevVelocity, currVelocity) != 0)
|
||||
{
|
||||
// Rewarding rhythm, take the smaller velocity as base.
|
||||
double rhythmBonus = Math.Min(currVector.Length, prevVector.Length);
|
||||
velChangeBonus = Math.Max(Math.Abs(prevVelocity - currVelocity) // reward for % distance slowed down compared to previous, paying attention to not award overlap
|
||||
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, osuCurrObj.JumpDistance / 100)), 2) // do not award overlap
|
||||
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2), // scale with ratio of difference compared to max
|
||||
Math.Min(125 / Math.Min(osuCurrObj.StrainTime, osuPrevObj.StrainTime), Math.Abs(prevVelocity - currVelocity)) // reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
|
||||
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2)); // scale with ratio of difference compared to max
|
||||
|
||||
if (osuCurrObj.StrainTime + 10 < osuPrevObj.StrainTime && osuPrevObj.StrainTime > osuLastObj.StrainTime + 10)
|
||||
// Don't want to reward for a rhythm change back to back (unless its a double, which is why this only checks for fast -> slow -> fast).
|
||||
rhythmBonus = 0;
|
||||
|
||||
aimStrain += rhythmBonus * rhythm_variance_multiplier; // add in rhythm velocity.
|
||||
velChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuPrevObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuPrevObj.StrainTime), 2); // penalize for rhythm changes.
|
||||
}
|
||||
|
||||
if (osuCurrObj.TravelTime != 0)
|
||||
{
|
||||
sliderBonus = osuCurrObj.TravelDistance / osuCurrObj.TravelTime; // add some slider rewards
|
||||
}
|
||||
|
||||
aimStrain += Math.Max(angleBonus, velChangeBonus * vel_change_multiplier); // Add in angle bonus or velchange bonus, whichever is larger.
|
||||
aimStrain += sliderBonus * slider_multiplier; // Add in additional slider velocity.
|
||||
|
||||
return aimStrain;
|
||||
}
|
||||
|
||||
private double calcWideAngleBonus(double angle)
|
||||
private double calcWideAngleBonus(double angle) => Math.Pow(Math.Sin(3.0 / 4 * (Math.Min(5.0 / 6 * Math.PI, Math.Max(Math.PI / 6, angle)) - Math.PI / 6)), 2);
|
||||
|
||||
private double calcAcuteAngleBonus(double angle) => 1 - calcWideAngleBonus(angle);
|
||||
|
||||
private double applyDiminishingExp(double val) => Math.Pow(val, 0.99);
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
|
||||
protected override double CalculateInitialStrain(double time) => currentStrain * strainDecay(time - Previous[0].StartTime);
|
||||
|
||||
protected override double StrainValueAt(DifficultyHitObject current)
|
||||
{
|
||||
if (angle < Math.PI / 3)
|
||||
return 0;
|
||||
if (angle < 2 * Math.PI / 3)
|
||||
return Math.Pow(Math.Sin(1.5 * (angle - Math.PI / 3)), 2);
|
||||
currentStrain *= strainDecay(current.DeltaTime);
|
||||
currentStrain += strainValueOf(current) * skillMultiplier;
|
||||
|
||||
return 0.25 + 0.75 * Math.Pow(Math.Sin(1.5 * (Math.PI - angle)), 2);
|
||||
}
|
||||
|
||||
private double calcAcuteAngleBonus(double angle)
|
||||
{
|
||||
if (angle < Math.PI / 3)
|
||||
return 0.5 + 0.5 * Math.Pow(Math.Sin(1.5 * angle), 2);
|
||||
if (angle < 2 * Math.PI / 3)
|
||||
return Math.Pow(Math.Sin(1.5 * (2 * Math.PI / 3 - angle)), 2);
|
||||
|
||||
return 0;
|
||||
return currentStrain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -15,20 +15,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
/// </summary>
|
||||
public class Speed : OsuStrainSkill
|
||||
{
|
||||
private const double single_spacing_threshold = 135;
|
||||
|
||||
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 single_spacing_threshold = 125;
|
||||
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.0))
|
||||
/ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
double od = context.Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty;
|
||||
double od = context.Beatmap.Difficulty.OverallDifficulty;
|
||||
|
||||
// These are meant to reflect the duration necessary for auto to score at least 1000 points on the spinner.
|
||||
// It's difficult to eliminate warnings here, as auto achieving 1000 points depends on the approach angle on some spinners.
|
||||
|
@ -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;
|
||||
}
|
||||
|
76
osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
Normal file
76
osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
#region Reduce AR (IApplicableToDifficulty)
|
||||
|
||||
public void ReadFromDifficulty(BeatmapDifficulty difficulty)
|
||||
public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
}
|
||||
|
||||
@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
.Select(beat =>
|
||||
{
|
||||
var newCircle = new HitCircle();
|
||||
newCircle.ApplyDefaults(controlPointInfo, osuBeatmap.BeatmapInfo.BaseDifficulty);
|
||||
newCircle.ApplyDefaults(controlPointInfo, osuBeatmap.Difficulty);
|
||||
newCircle.StartTime = beat;
|
||||
return (OsuHitObject)newCircle;
|
||||
}).ToList();
|
||||
|
@ -122,11 +122,11 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
});
|
||||
}
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN);
|
||||
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN);
|
||||
|
||||
// Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR.
|
||||
// This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above.
|
||||
|
@ -79,6 +79,12 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
/// </summary>
|
||||
internal float LazyTravelDistance;
|
||||
|
||||
/// <summary>
|
||||
/// The time taken by the cursor upon completion of this <see cref="Slider"/> if it was hit
|
||||
/// with as few movements as possible. This is set and used by difficulty calculation.
|
||||
/// </summary>
|
||||
internal double LazyTravelTime;
|
||||
|
||||
public List<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>();
|
||||
|
||||
[JsonIgnore]
|
||||
@ -135,14 +141,13 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
Path.Version.ValueChanged += _ => updateNestedPositions();
|
||||
}
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
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 +180,6 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
StartTime = e.Time,
|
||||
Position = Position,
|
||||
StackHeight = StackHeight,
|
||||
SampleControlPoint = SampleControlPoint,
|
||||
});
|
||||
break;
|
||||
|
||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
public double SpanDuration => slider.SpanDuration;
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
public int SpanIndex { get; set; }
|
||||
public double SpanStartTime { get; set; }
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
|
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
/// </summary>
|
||||
public int MaximumBonusSpins { get; protected set; } = 1;
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
double secondsDuration = Duration / 1000;
|
||||
|
||||
double minimumRotationsPerSecond = stable_matching_fudge * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5);
|
||||
double minimumRotationsPerSecond = stable_matching_fudge * IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5);
|
||||
|
||||
SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond);
|
||||
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration);
|
||||
|
@ -192,6 +192,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
new OsuModBarrelRoll(),
|
||||
new OsuModApproachDifferent(),
|
||||
new OsuModMuted(),
|
||||
new OsuModNoScope(),
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
|
@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Replays
|
||||
: base(beatmap, mods)
|
||||
{
|
||||
defaultHitWindows = new OsuHitWindows();
|
||||
defaultHitWindows.SetDifficulty(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||
defaultHitWindows.SetDifficulty(Beatmap.Difficulty.OverallDifficulty);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
private OsuPlayfield playfield { get; set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private GameplayBeatmap gameplayBeatmap { get; set; }
|
||||
private GameplayState gameplayState { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin, OsuColour colours)
|
||||
@ -75,12 +75,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
if (playfield == null || gameplayBeatmap == null) return;
|
||||
if (playfield == null || gameplayState == null) return;
|
||||
|
||||
DrawableHitObject kiaiHitObject = null;
|
||||
|
||||
// Check whether currently in a kiai section first. This is only done as an optimisation to avoid enumerating AliveObjects when not necessary.
|
||||
if (gameplayBeatmap.ControlPointInfo.EffectPointAt(Time.Current).KiaiMode)
|
||||
if (gameplayState.Beatmap.ControlPointInfo.EffectPointAt(Time.Current).KiaiMode)
|
||||
kiaiHitObject = playfield.HitObjectContainer.AliveObjects.FirstOrDefault(isTracking);
|
||||
|
||||
kiaiSpewer.Active.Value = kiaiHitObject != null;
|
||||
|
@ -35,8 +35,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
private Drawable hitCircleSprite;
|
||||
|
||||
protected Drawable HitCircleOverlay { get; private set; }
|
||||
protected Container OverlayLayer { get; private set; }
|
||||
|
||||
private Drawable hitCircleOverlay;
|
||||
private SkinnableSpriteText hitCircleText;
|
||||
|
||||
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
|
||||
@ -78,17 +79,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
HitCircleOverlay = new KiaiFlashingSprite
|
||||
OverlayLayer = new Container
|
||||
{
|
||||
Texture = overlayTexture,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
Child = hitCircleOverlay = new KiaiFlashingSprite
|
||||
{
|
||||
Texture = overlayTexture,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
if (hasNumber)
|
||||
{
|
||||
AddInternal(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
|
||||
OverlayLayer.Add(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Numeric.With(size: 40),
|
||||
UseFullGlyphHeight = false,
|
||||
@ -102,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
|
||||
|
||||
if (overlayAboveNumber)
|
||||
ChangeInternalChildDepth(HitCircleOverlay, float.MinValue);
|
||||
OverlayLayer.ChangeChildDepth(hitCircleOverlay, float.MinValue);
|
||||
|
||||
accentColour.BindTo(drawableObject.AccentColour);
|
||||
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
|
||||
@ -147,8 +153,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
hitCircleSprite.FadeOut(legacy_fade_duration, Easing.Out);
|
||||
hitCircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
|
||||
|
||||
HitCircleOverlay.FadeOut(legacy_fade_duration, Easing.Out);
|
||||
HitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
|
||||
hitCircleOverlay.FadeOut(legacy_fade_duration, Easing.Out);
|
||||
hitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
|
||||
|
||||
if (hasNumber)
|
||||
{
|
||||
|
@ -13,26 +13,20 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
public class LegacyReverseArrow : CompositeDrawable
|
||||
{
|
||||
private ISkin skin { get; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private DrawableHitObject drawableHitObject { get; set; }
|
||||
|
||||
private Drawable proxy;
|
||||
|
||||
public LegacyReverseArrow(ISkin skin)
|
||||
{
|
||||
this.skin = skin;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(ISkinSource skinSource)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
string lookupName = new OsuSkinComponent(OsuSkinComponents.ReverseArrow).LookupName;
|
||||
|
||||
InternalChild = skin.GetAnimation(lookupName, true, true) ?? Empty();
|
||||
var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null);
|
||||
InternalChild = skin?.GetAnimation(lookupName, true, true) ?? Empty();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
[Resolved(canBeNull: true)]
|
||||
private DrawableHitObject drawableHitObject { get; set; }
|
||||
|
||||
private Drawable proxiedHitCircleOverlay;
|
||||
private Drawable proxiedOverlayLayer;
|
||||
|
||||
public LegacySliderHeadHitCircle()
|
||||
: base("sliderstartcircle")
|
||||
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
proxiedHitCircleOverlay = HitCircleOverlay.CreateProxy();
|
||||
proxiedOverlayLayer = OverlayLayer.CreateProxy();
|
||||
|
||||
if (drawableHitObject != null)
|
||||
{
|
||||
@ -35,11 +35,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
private void onHitObjectApplied(DrawableHitObject drawableObject)
|
||||
{
|
||||
Debug.Assert(proxiedHitCircleOverlay.Parent == null);
|
||||
Debug.Assert(proxiedOverlayLayer.Parent == null);
|
||||
|
||||
// see logic in LegacyReverseArrow.
|
||||
(drawableObject as DrawableSliderHead)?.DrawableSlider
|
||||
.OverlayElementContainer.Add(proxiedHitCircleOverlay.With(d => d.Depth = float.MinValue));
|
||||
.OverlayElementContainer.Add(proxiedOverlayLayer.With(d => d.Depth = float.MinValue));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
case OsuSkinComponents.ReverseArrow:
|
||||
if (hasHitCircle.Value)
|
||||
return new LegacyReverseArrow(this);
|
||||
return new LegacyReverseArrow();
|
||||
|
||||
return null;
|
||||
|
||||
|
@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
||||
return;
|
||||
|
||||
// Todo: This should probably not be done like this.
|
||||
float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5) / 5) / 2;
|
||||
float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.Difficulty.CircleSize - 5) / 5) / 2;
|
||||
|
||||
foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)))
|
||||
{
|
||||
|
@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
}
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private GameplayBeatmap beatmap { get; set; }
|
||||
private GameplayState state { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
@ -96,10 +96,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
{
|
||||
float scale = userCursorScale.Value;
|
||||
|
||||
if (autoCursorScale.Value && beatmap != null)
|
||||
if (autoCursorScale.Value && state != null)
|
||||
{
|
||||
// if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
|
||||
scale *= GetScaleForCircleSize(beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
|
||||
scale *= GetScaleForCircleSize(state.Beatmap.Difficulty.CircleSize);
|
||||
}
|
||||
|
||||
cursorScale.Value = scale;
|
||||
|
@ -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());
|
||||
|
||||
|
@ -10,6 +10,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Utils;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
@ -46,11 +47,10 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
|
||||
protected override Beatmap<TaikoHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!(original.BeatmapInfo.BaseDifficulty is TaikoMutliplierAppliedDifficulty))
|
||||
if (!(original.Difficulty is TaikoMultiplierAppliedDifficulty))
|
||||
{
|
||||
// Rewrite the beatmap info to add the slider velocity multiplier
|
||||
original.BeatmapInfo = original.BeatmapInfo.Clone();
|
||||
original.BeatmapInfo.BaseDifficulty = new TaikoMutliplierAppliedDifficulty(original.BeatmapInfo.BaseDifficulty);
|
||||
original.Difficulty = new TaikoMultiplierAppliedDifficulty(original.Difficulty);
|
||||
}
|
||||
|
||||
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original, cancellationToken);
|
||||
@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
StartTime = obj.StartTime,
|
||||
Samples = obj.Samples,
|
||||
Duration = taikoDuration,
|
||||
TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4
|
||||
TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4
|
||||
};
|
||||
}
|
||||
|
||||
@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
|
||||
case IHasDuration endTimeData:
|
||||
{
|
||||
double hitMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier;
|
||||
double hitMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier;
|
||||
|
||||
yield return new Swell
|
||||
{
|
||||
@ -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,12 +162,12 @@ 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.BeatmapInfo.BaseDifficulty.SliderMultiplier / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate;
|
||||
double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate;
|
||||
|
||||
// The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll.
|
||||
double taikoVelocity = sliderScoringPointDistance * beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate;
|
||||
double taikoVelocity = sliderScoringPointDistance * beatmap.Difficulty.SliderTickRate;
|
||||
taikoDuration = (int)(distance / taikoVelocity * beatLength);
|
||||
|
||||
if (isForCurrentRuleset)
|
||||
@ -183,7 +183,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
beatLength = timingPoint.BeatLength;
|
||||
|
||||
// If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
|
||||
tickSpacing = Math.Min(beatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, (double)taikoDuration / spans);
|
||||
tickSpacing = Math.Min(beatLength / beatmap.Difficulty.SliderTickRate, (double)taikoDuration / spans);
|
||||
|
||||
return tickSpacing > 0
|
||||
&& distance / osuVelocity * 1000 < 2 * beatLength;
|
||||
@ -191,13 +191,35 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
|
||||
protected override Beatmap<TaikoHitObject> CreateBeatmap() => new TaikoBeatmap();
|
||||
|
||||
private class TaikoMutliplierAppliedDifficulty : BeatmapDifficulty
|
||||
private class TaikoMultiplierAppliedDifficulty : BeatmapDifficulty
|
||||
{
|
||||
public TaikoMutliplierAppliedDifficulty(BeatmapDifficulty difficulty)
|
||||
public TaikoMultiplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
difficulty.CopyTo(this);
|
||||
SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
|
||||
CopyFrom(difficulty);
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
public TaikoMultiplierAppliedDifficulty()
|
||||
{
|
||||
}
|
||||
|
||||
#region Overrides of BeatmapDifficulty
|
||||
|
||||
public override void CopyTo(BeatmapDifficulty other)
|
||||
{
|
||||
base.CopyTo(other);
|
||||
if (!(other is TaikoMultiplierAppliedDifficulty))
|
||||
SliderMultiplier /= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
|
||||
}
|
||||
|
||||
public override void CopyFrom(IBeatmapDifficultyInfo other)
|
||||
{
|
||||
base.CopyFrom(other);
|
||||
if (!(other is TaikoMultiplierAppliedDifficulty))
|
||||
SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
starRating = rescale(starRating);
|
||||
|
||||
HitWindows hitWindows = new TaikoHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
return new TaikoDifficultyAttributes
|
||||
{
|
||||
@ -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
|
||||
};
|
||||
|
@ -58,14 +58,13 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
|
||||
private float overallDifficulty = BeatmapDifficulty.DEFAULT_DIFFICULTY;
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
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;
|
||||
|
@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Taiko.Scoring
|
||||
{
|
||||
base.ApplyBeatmap(beatmap);
|
||||
|
||||
hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType<Hit>().Count()) * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98));
|
||||
hpMissMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120);
|
||||
hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType<Hit>().Count()) * IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.5, 0.75, 0.98));
|
||||
hpMissMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.0018, 0.0075, 0.0120);
|
||||
}
|
||||
|
||||
protected override double GetHealthIncreaseFor(JudgementResult result)
|
||||
|
@ -25,10 +25,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(GameplayBeatmap gameplayBeatmap)
|
||||
private void load(GameplayState gameplayState)
|
||||
{
|
||||
if (gameplayBeatmap != null)
|
||||
((IBindable<JudgementResult>)LastResult).BindTo(gameplayBeatmap.LastJudgementResult);
|
||||
if (gameplayState != null)
|
||||
((IBindable<JudgementResult>)LastResult).BindTo(gameplayState.LastJudgementResult);
|
||||
}
|
||||
|
||||
private bool passing;
|
||||
|
@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(TextureStore textures, GameplayBeatmap gameplayBeatmap)
|
||||
private void load(TextureStore textures, GameplayState gameplayState)
|
||||
{
|
||||
InternalChildren = new[]
|
||||
{
|
||||
@ -49,8 +49,8 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
animations[TaikoMascotAnimationState.Fail] = new TaikoMascotAnimation(TaikoMascotAnimationState.Fail),
|
||||
};
|
||||
|
||||
if (gameplayBeatmap != null)
|
||||
((IBindable<JudgementResult>)LastResult).BindTo(gameplayBeatmap.LastJudgementResult);
|
||||
if (gameplayState != null)
|
||||
((IBindable<JudgementResult>)LastResult).BindTo(gameplayState.LastJudgementResult);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
@ -129,7 +129,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var difficulty = decoder.Decode(stream).BeatmapInfo.BaseDifficulty;
|
||||
var difficulty = decoder.Decode(stream).Difficulty;
|
||||
|
||||
Assert.AreEqual(6.5f, difficulty.DrainRate);
|
||||
Assert.AreEqual(4, difficulty.CircleSize);
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -149,5 +149,32 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
Assert.AreEqual(AnimationLoopType.LoopForever, ((StoryboardAnimation)foreground.Elements[5]).LoopType);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDecodeLoopCount()
|
||||
{
|
||||
// all loop sequences in loop-count.osb have a total duration of 2000ms (fade in 0->1000ms, fade out 1000->2000ms).
|
||||
const double loop_duration = 2000;
|
||||
|
||||
var decoder = new LegacyStoryboardDecoder();
|
||||
|
||||
using (var resStream = TestResources.OpenResource("loop-count.osb"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var storyboard = decoder.Decode(stream);
|
||||
|
||||
StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
|
||||
|
||||
// stable ensures that any loop command executes at least once, even if the loop count specified in the .osb is zero or negative.
|
||||
StoryboardSprite zeroTimes = background.Elements.OfType<StoryboardSprite>().Single(s => s.Path == "zero-times.png");
|
||||
Assert.That(zeroTimes.EndTime, Is.EqualTo(1000 + loop_duration));
|
||||
|
||||
StoryboardSprite oneTime = background.Elements.OfType<StoryboardSprite>().Single(s => s.Path == "one-time.png");
|
||||
Assert.That(oneTime.EndTime, Is.EqualTo(4000 + loop_duration));
|
||||
|
||||
StoryboardSprite manyTimes = background.Elements.OfType<StoryboardSprite>().Single(s => s.Path == "many-times.png");
|
||||
Assert.That(manyTimes.EndTime, Is.EqualTo(9000 + 40 * loop_duration));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
public void TestDecodeDifficulty()
|
||||
{
|
||||
var beatmap = decodeAsJson(normal);
|
||||
var difficulty = beatmap.BeatmapInfo.BaseDifficulty;
|
||||
var difficulty = beatmap.Difficulty;
|
||||
Assert.AreEqual(6.5f, difficulty.DrainRate);
|
||||
Assert.AreEqual(4, difficulty.CircleSize);
|
||||
Assert.AreEqual(8, difficulty.OverallDifficulty);
|
||||
@ -102,7 +102,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
|
||||
processor.PreProcess();
|
||||
foreach (var o in converted.HitObjects)
|
||||
o.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty);
|
||||
o.ApplyDefaults(converted.ControlPointInfo, converted.Difficulty);
|
||||
processor.PostProcess();
|
||||
|
||||
var beatmap = converted.Serialize().Deserialize<Beatmap>();
|
||||
|
@ -86,7 +86,7 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
|
||||
var manager = osu.Dependencies.Get<BeatmapManager>();
|
||||
|
||||
BeatmapSetInfo importedSet;
|
||||
ILive<BeatmapSetInfo> importedSet;
|
||||
|
||||
using (var stream = File.OpenRead(tempPath))
|
||||
{
|
||||
@ -97,7 +97,7 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing");
|
||||
File.Delete(tempPath);
|
||||
|
||||
var imported = manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID);
|
||||
var imported = manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID);
|
||||
|
||||
deleteBeatmapSet(imported, osu);
|
||||
}
|
||||
@ -172,8 +172,8 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
ensureLoaded(osu);
|
||||
|
||||
// but contents doesn't, so existing should still be used.
|
||||
Assert.IsTrue(imported.ID == importedSecondTime.ID);
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
|
||||
Assert.IsTrue(imported.ID == importedSecondTime.Value.ID);
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Value.Beatmaps.First().ID);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -226,8 +226,8 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
ensureLoaded(osu);
|
||||
|
||||
// check the newly "imported" beatmap is not the original.
|
||||
Assert.IsTrue(imported.ID != importedSecondTime.ID);
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID);
|
||||
Assert.IsTrue(imported.ID != importedSecondTime.Value.ID);
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -278,8 +278,8 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
ensureLoaded(osu);
|
||||
|
||||
// check the newly "imported" beatmap is not the original.
|
||||
Assert.IsTrue(imported.ID != importedSecondTime.ID);
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID);
|
||||
Assert.IsTrue(imported.ID != importedSecondTime.Value.ID);
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -329,8 +329,8 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
ensureLoaded(osu);
|
||||
|
||||
// check the newly "imported" beatmap is not the original.
|
||||
Assert.IsTrue(imported.ID != importedSecondTime.ID);
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID);
|
||||
Assert.IsTrue(imported.ID != importedSecondTime.Value.ID);
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -570,8 +570,8 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
var imported = await manager.Import(toImport);
|
||||
|
||||
Assert.NotNull(imported);
|
||||
Assert.AreEqual(null, imported.Beatmaps[0].OnlineBeatmapID);
|
||||
Assert.AreEqual(null, imported.Beatmaps[1].OnlineBeatmapID);
|
||||
Assert.AreEqual(null, imported.Value.Beatmaps[0].OnlineBeatmapID);
|
||||
Assert.AreEqual(null, imported.Value.Beatmaps[1].OnlineBeatmapID);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -582,7 +582,6 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
|
||||
[Test]
|
||||
[NonParallelizable]
|
||||
[Ignore("Binding IPC on Appveyor isn't working (port in use). Need to figure out why")]
|
||||
public void TestImportOverIPC()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-host", true))
|
||||
@ -707,7 +706,7 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
|
||||
ensureLoaded(osu);
|
||||
|
||||
Assert.IsFalse(imported.Files.Any(f => f.Filename.Contains("subfolder")), "Files contain common subfolder");
|
||||
Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("subfolder")), "Files contain common subfolder");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -760,8 +759,8 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
|
||||
ensureLoaded(osu);
|
||||
|
||||
Assert.IsFalse(imported.Files.Any(f => f.Filename.Contains("__MACOSX")), "Files contain resource fork folder, which should be ignored");
|
||||
Assert.IsFalse(imported.Files.Any(f => f.Filename.Contains("actual_data")), "Files contain common subfolder");
|
||||
Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("__MACOSX")), "Files contain resource fork folder, which should be ignored");
|
||||
Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("actual_data")), "Files contain common subfolder");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -910,13 +909,13 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
|
||||
var manager = osu.Dependencies.Get<BeatmapManager>();
|
||||
|
||||
var importedSet = await manager.Import(new ImportTask(temp));
|
||||
var importedSet = await manager.Import(new ImportTask(temp)).ConfigureAwait(false);
|
||||
|
||||
ensureLoaded(osu);
|
||||
|
||||
waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
|
||||
|
||||
return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID);
|
||||
return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID);
|
||||
}
|
||||
|
||||
public static async Task<BeatmapSetInfo> LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false)
|
||||
@ -925,13 +924,13 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
|
||||
var manager = osu.Dependencies.Get<BeatmapManager>();
|
||||
|
||||
var importedSet = await manager.Import(new ImportTask(temp));
|
||||
var importedSet = await manager.Import(new ImportTask(temp)).ConfigureAwait(false);
|
||||
|
||||
ensureLoaded(osu);
|
||||
|
||||
waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
|
||||
|
||||
return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID);
|
||||
return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID);
|
||||
}
|
||||
|
||||
private void deleteBeatmapSet(BeatmapSetInfo imported, OsuGameBase osu)
|
||||
@ -946,13 +945,13 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending);
|
||||
}
|
||||
|
||||
private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmap)
|
||||
private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmapInfo)
|
||||
{
|
||||
return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
|
||||
{
|
||||
OnlineScoreID = 2,
|
||||
Beatmap = beatmap,
|
||||
BeatmapInfoID = beatmap.ID
|
||||
BeatmapInfo = beatmapInfo,
|
||||
BeatmapInfoID = beatmapInfo.ID
|
||||
}, new ImportScoreTest.TestArchiveReader());
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
820
osu.Game.Tests/Database/BeatmapImporterTests.cs
Normal file
820
osu.Game.Tests/Database/BeatmapImporterTests.cs
Normal file
@ -0,0 +1,820 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Stores;
|
||||
using osu.Game.Tests.Resources;
|
||||
using Realms;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Archives.Zip;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Writers.Zip;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Tests.Database
|
||||
{
|
||||
[TestFixture]
|
||||
public class BeatmapImporterTests : RealmTest
|
||||
{
|
||||
[Test]
|
||||
public void TestImportBeatmapThenCleanup()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using (var importer = new BeatmapImporter(realmFactory, storage))
|
||||
using (new RealmRulesetStore(realmFactory, storage))
|
||||
{
|
||||
ILive<RealmBeatmapSet>? imported;
|
||||
|
||||
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
|
||||
imported = await importer.Import(reader);
|
||||
|
||||
Assert.AreEqual(1, realmFactory.Context.All<RealmBeatmapSet>().Count());
|
||||
|
||||
Assert.NotNull(imported);
|
||||
Debug.Assert(imported != null);
|
||||
|
||||
imported.PerformWrite(s => s.DeletePending = true);
|
||||
|
||||
Assert.AreEqual(1, realmFactory.Context.All<RealmBeatmapSet>().Count(s => s.DeletePending));
|
||||
}
|
||||
});
|
||||
|
||||
Logger.Log("Running with no work to purge pending deletions");
|
||||
|
||||
RunTestWithRealm((realmFactory, _) => { Assert.AreEqual(0, realmFactory.Context.All<RealmBeatmapSet>().Count()); });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportWhenClosed()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportThenDelete()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
|
||||
deleteBeatmapSet(imported, realmFactory.Context);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportThenDeleteFromStream()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var tempPath = TestResources.GetTestBeatmapForImport();
|
||||
|
||||
ILive<RealmBeatmapSet>? importedSet;
|
||||
|
||||
using (var stream = File.OpenRead(tempPath))
|
||||
{
|
||||
importedSet = await importer.Import(new ImportTask(stream, Path.GetFileName(tempPath)));
|
||||
ensureLoaded(realmFactory.Context);
|
||||
}
|
||||
|
||||
Assert.NotNull(importedSet);
|
||||
Debug.Assert(importedSet != null);
|
||||
|
||||
Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing");
|
||||
File.Delete(tempPath);
|
||||
|
||||
var imported = realmFactory.Context.All<RealmBeatmapSet>().First(beatmapSet => beatmapSet.ID == importedSet.ID);
|
||||
|
||||
deleteBeatmapSet(imported, realmFactory.Context);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportThenImport()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
|
||||
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
|
||||
Assert.IsTrue(imported.ID == importedSecondTime.ID);
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
|
||||
|
||||
checkBeatmapSetCount(realmFactory.Context, 1);
|
||||
checkSingleReferencedFileCount(realmFactory.Context, 18);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportThenImportWithReZip()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
|
||||
string extractedFolder = $"{temp}_extracted";
|
||||
Directory.CreateDirectory(extractedFolder);
|
||||
|
||||
try
|
||||
{
|
||||
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
|
||||
string hashBefore = hashFile(temp);
|
||||
|
||||
using (var zip = ZipArchive.Open(temp))
|
||||
zip.WriteToDirectory(extractedFolder);
|
||||
|
||||
using (var zip = ZipArchive.Create())
|
||||
{
|
||||
zip.AddAllFromDirectory(extractedFolder);
|
||||
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
|
||||
}
|
||||
|
||||
// zip files differ because different compression or encoder.
|
||||
Assert.AreNotEqual(hashBefore, hashFile(temp));
|
||||
|
||||
var importedSecondTime = await importer.Import(new ImportTask(temp));
|
||||
|
||||
ensureLoaded(realmFactory.Context);
|
||||
|
||||
Assert.NotNull(importedSecondTime);
|
||||
Debug.Assert(importedSecondTime != null);
|
||||
|
||||
// but contents doesn't, so existing should still be used.
|
||||
Assert.IsTrue(imported.ID == importedSecondTime.ID);
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.PerformRead(s => s.Beatmaps.First().ID));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(extractedFolder, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportThenImportWithChangedHashedFile()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
|
||||
string extractedFolder = $"{temp}_extracted";
|
||||
Directory.CreateDirectory(extractedFolder);
|
||||
|
||||
try
|
||||
{
|
||||
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
|
||||
await createScoreForBeatmap(realmFactory.Context, imported.Beatmaps.First());
|
||||
|
||||
using (var zip = ZipArchive.Open(temp))
|
||||
zip.WriteToDirectory(extractedFolder);
|
||||
|
||||
// arbitrary write to hashed file
|
||||
// this triggers the special BeatmapManager.PreImport deletion/replacement flow.
|
||||
using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.osu").First()).AppendText())
|
||||
await sw.WriteLineAsync("// changed");
|
||||
|
||||
using (var zip = ZipArchive.Create())
|
||||
{
|
||||
zip.AddAllFromDirectory(extractedFolder);
|
||||
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
|
||||
}
|
||||
|
||||
var importedSecondTime = await importer.Import(new ImportTask(temp));
|
||||
|
||||
ensureLoaded(realmFactory.Context);
|
||||
|
||||
// check the newly "imported" beatmap is not the original.
|
||||
Assert.NotNull(importedSecondTime);
|
||||
Debug.Assert(importedSecondTime != null);
|
||||
|
||||
Assert.IsTrue(imported.ID != importedSecondTime.ID);
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.PerformRead(s => s.Beatmaps.First().ID));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(extractedFolder, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("intentionally broken by import optimisations")]
|
||||
public void TestImportThenImportWithChangedFile()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
|
||||
string extractedFolder = $"{temp}_extracted";
|
||||
Directory.CreateDirectory(extractedFolder);
|
||||
|
||||
try
|
||||
{
|
||||
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
|
||||
using (var zip = ZipArchive.Open(temp))
|
||||
zip.WriteToDirectory(extractedFolder);
|
||||
|
||||
// arbitrary write to non-hashed file
|
||||
using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.mp3").First()).AppendText())
|
||||
await sw.WriteLineAsync("text");
|
||||
|
||||
using (var zip = ZipArchive.Create())
|
||||
{
|
||||
zip.AddAllFromDirectory(extractedFolder);
|
||||
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
|
||||
}
|
||||
|
||||
var importedSecondTime = await importer.Import(new ImportTask(temp));
|
||||
|
||||
ensureLoaded(realmFactory.Context);
|
||||
|
||||
Assert.NotNull(importedSecondTime);
|
||||
Debug.Assert(importedSecondTime != null);
|
||||
|
||||
// check the newly "imported" beatmap is not the original.
|
||||
Assert.IsTrue(imported.ID != importedSecondTime.ID);
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.PerformRead(s => s.Beatmaps.First().ID));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(extractedFolder, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportThenImportWithDifferentFilename()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
|
||||
string extractedFolder = $"{temp}_extracted";
|
||||
Directory.CreateDirectory(extractedFolder);
|
||||
|
||||
try
|
||||
{
|
||||
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
|
||||
using (var zip = ZipArchive.Open(temp))
|
||||
zip.WriteToDirectory(extractedFolder);
|
||||
|
||||
// change filename
|
||||
var firstFile = new FileInfo(Directory.GetFiles(extractedFolder).First());
|
||||
firstFile.MoveTo(Path.Combine(firstFile.DirectoryName.AsNonNull(), $"{firstFile.Name}-changed{firstFile.Extension}"));
|
||||
|
||||
using (var zip = ZipArchive.Create())
|
||||
{
|
||||
zip.AddAllFromDirectory(extractedFolder);
|
||||
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
|
||||
}
|
||||
|
||||
var importedSecondTime = await importer.Import(new ImportTask(temp));
|
||||
|
||||
ensureLoaded(realmFactory.Context);
|
||||
|
||||
Assert.NotNull(importedSecondTime);
|
||||
Debug.Assert(importedSecondTime != null);
|
||||
|
||||
// check the newly "imported" beatmap is not the original.
|
||||
Assert.IsTrue(imported.ID != importedSecondTime.ID);
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.PerformRead(s => s.Beatmaps.First().ID));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(extractedFolder, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("intentionally broken by import optimisations")]
|
||||
public void TestImportCorruptThenImport()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
|
||||
var firstFile = imported.Files.First();
|
||||
|
||||
long originalLength;
|
||||
using (var stream = storage.GetStream(firstFile.File.StoragePath))
|
||||
originalLength = stream.Length;
|
||||
|
||||
using (var stream = storage.GetStream(firstFile.File.StoragePath, FileAccess.Write, FileMode.Create))
|
||||
stream.WriteByte(0);
|
||||
|
||||
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
|
||||
using (var stream = storage.GetStream(firstFile.File.StoragePath))
|
||||
Assert.AreEqual(stream.Length, originalLength, "Corruption was not fixed on second import");
|
||||
|
||||
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
|
||||
Assert.IsTrue(imported.ID == importedSecondTime.ID);
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
|
||||
|
||||
checkBeatmapSetCount(realmFactory.Context, 1);
|
||||
checkSingleReferencedFileCount(realmFactory.Context, 18);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRollbackOnFailure()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
int loggedExceptionCount = 0;
|
||||
|
||||
Logger.NewEntry += l =>
|
||||
{
|
||||
if (l.Target == LoggingTarget.Database && l.Exception != null)
|
||||
Interlocked.Increment(ref loggedExceptionCount);
|
||||
};
|
||||
|
||||
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
|
||||
realmFactory.Context.Write(() => imported.Hash += "-changed");
|
||||
|
||||
checkBeatmapSetCount(realmFactory.Context, 1);
|
||||
checkBeatmapCount(realmFactory.Context, 12);
|
||||
checkSingleReferencedFileCount(realmFactory.Context, 18);
|
||||
|
||||
var brokenTempFilename = TestResources.GetTestBeatmapForImport();
|
||||
|
||||
MemoryStream brokenOsu = new MemoryStream();
|
||||
MemoryStream brokenOsz = new MemoryStream(await File.ReadAllBytesAsync(brokenTempFilename));
|
||||
|
||||
File.Delete(brokenTempFilename);
|
||||
|
||||
using (var outStream = File.Open(brokenTempFilename, FileMode.CreateNew))
|
||||
using (var zip = ZipArchive.Open(brokenOsz))
|
||||
{
|
||||
zip.AddEntry("broken.osu", brokenOsu, false);
|
||||
zip.SaveTo(outStream, CompressionType.Deflate);
|
||||
}
|
||||
|
||||
// this will trigger purging of the existing beatmap (online set id match) but should rollback due to broken osu.
|
||||
try
|
||||
{
|
||||
await importer.Import(new ImportTask(brokenTempFilename));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
checkBeatmapSetCount(realmFactory.Context, 1);
|
||||
checkBeatmapCount(realmFactory.Context, 12);
|
||||
|
||||
checkSingleReferencedFileCount(realmFactory.Context, 18);
|
||||
|
||||
Assert.AreEqual(1, loggedExceptionCount);
|
||||
|
||||
File.Delete(brokenTempFilename);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportThenDeleteThenImport()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
|
||||
deleteBeatmapSet(imported, realmFactory.Context);
|
||||
|
||||
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
|
||||
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
|
||||
Assert.IsTrue(imported.ID == importedSecondTime.ID);
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportThenDeleteThenImportWithOnlineIDsMissing()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
|
||||
realmFactory.Context.Write(() =>
|
||||
{
|
||||
foreach (var b in imported.Beatmaps)
|
||||
b.OnlineID = -1;
|
||||
});
|
||||
|
||||
deleteBeatmapSet(imported, realmFactory.Context);
|
||||
|
||||
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
|
||||
// check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched)
|
||||
Assert.IsTrue(imported.ID != importedSecondTime.ID);
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportWithDuplicateBeatmapIDs()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var metadata = new RealmBeatmapMetadata
|
||||
{
|
||||
Artist = "SomeArtist",
|
||||
Author = "SomeAuthor"
|
||||
};
|
||||
|
||||
var ruleset = realmFactory.Context.All<RealmRuleset>().First();
|
||||
|
||||
var toImport = new RealmBeatmapSet
|
||||
{
|
||||
OnlineID = 1,
|
||||
Beatmaps =
|
||||
{
|
||||
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata)
|
||||
{
|
||||
OnlineID = 2,
|
||||
},
|
||||
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata)
|
||||
{
|
||||
OnlineID = 2,
|
||||
Status = BeatmapSetOnlineStatus.Loved,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var imported = await importer.Import(toImport);
|
||||
|
||||
Assert.NotNull(imported);
|
||||
Debug.Assert(imported != null);
|
||||
|
||||
Assert.AreEqual(-1, imported.PerformRead(s => s.Beatmaps[0].OnlineID));
|
||||
Assert.AreEqual(-1, imported.PerformRead(s => s.Beatmaps[1].OnlineID));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportWhenFileOpen()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
using (File.OpenRead(temp))
|
||||
await importer.Import(temp);
|
||||
ensureLoaded(realmFactory.Context);
|
||||
File.Delete(temp);
|
||||
Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportWithDuplicateHashes()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
|
||||
string extractedFolder = $"{temp}_extracted";
|
||||
Directory.CreateDirectory(extractedFolder);
|
||||
|
||||
try
|
||||
{
|
||||
using (var zip = ZipArchive.Open(temp))
|
||||
zip.WriteToDirectory(extractedFolder);
|
||||
|
||||
using (var zip = ZipArchive.Create())
|
||||
{
|
||||
zip.AddAllFromDirectory(extractedFolder);
|
||||
zip.AddEntry("duplicate.osu", Directory.GetFiles(extractedFolder, "*.osu").First());
|
||||
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
|
||||
}
|
||||
|
||||
await importer.Import(temp);
|
||||
|
||||
ensureLoaded(realmFactory.Context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(extractedFolder, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportNestedStructure()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
|
||||
string extractedFolder = $"{temp}_extracted";
|
||||
string subfolder = Path.Combine(extractedFolder, "subfolder");
|
||||
|
||||
Directory.CreateDirectory(subfolder);
|
||||
|
||||
try
|
||||
{
|
||||
using (var zip = ZipArchive.Open(temp))
|
||||
zip.WriteToDirectory(subfolder);
|
||||
|
||||
using (var zip = ZipArchive.Create())
|
||||
{
|
||||
zip.AddAllFromDirectory(extractedFolder);
|
||||
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
|
||||
}
|
||||
|
||||
var imported = await importer.Import(new ImportTask(temp));
|
||||
|
||||
Assert.NotNull(imported);
|
||||
Debug.Assert(imported != null);
|
||||
|
||||
ensureLoaded(realmFactory.Context);
|
||||
|
||||
Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("subfolder"))), "Files contain common subfolder");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(extractedFolder, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportWithIgnoredDirectoryInArchive()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
|
||||
string extractedFolder = $"{temp}_extracted";
|
||||
string dataFolder = Path.Combine(extractedFolder, "actual_data");
|
||||
string resourceForkFolder = Path.Combine(extractedFolder, "__MACOSX");
|
||||
string resourceForkFilePath = Path.Combine(resourceForkFolder, ".extracted");
|
||||
|
||||
Directory.CreateDirectory(dataFolder);
|
||||
Directory.CreateDirectory(resourceForkFolder);
|
||||
|
||||
using (var resourceForkFile = File.CreateText(resourceForkFilePath))
|
||||
{
|
||||
await resourceForkFile.WriteLineAsync("adding content so that it's not empty");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using (var zip = ZipArchive.Open(temp))
|
||||
zip.WriteToDirectory(dataFolder);
|
||||
|
||||
using (var zip = ZipArchive.Create())
|
||||
{
|
||||
zip.AddAllFromDirectory(extractedFolder);
|
||||
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
|
||||
}
|
||||
|
||||
var imported = await importer.Import(new ImportTask(temp));
|
||||
|
||||
Assert.NotNull(imported);
|
||||
Debug.Assert(imported != null);
|
||||
|
||||
ensureLoaded(realmFactory.Context);
|
||||
|
||||
Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("__MACOSX"))), "Files contain resource fork folder, which should be ignored");
|
||||
Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("actual_data"))), "Files contain common subfolder");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(extractedFolder, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUpdateBeatmapInfo()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
await importer.Import(temp);
|
||||
|
||||
// Update via the beatmap, not the beatmap info, to ensure correct linking
|
||||
RealmBeatmapSet setToUpdate = realmFactory.Context.All<RealmBeatmapSet>().First();
|
||||
|
||||
var beatmapToUpdate = setToUpdate.Beatmaps.First();
|
||||
|
||||
realmFactory.Context.Write(() => beatmapToUpdate.DifficultyName = "updated");
|
||||
|
||||
RealmBeatmap updatedInfo = realmFactory.Context.All<RealmBeatmap>().First(b => b.ID == beatmapToUpdate.ID);
|
||||
Assert.That(updatedInfo.DifficultyName, Is.EqualTo("updated"));
|
||||
});
|
||||
}
|
||||
|
||||
public static async Task<RealmBeatmapSet?> LoadQuickOszIntoOsu(BeatmapImporter importer, Realm realm)
|
||||
{
|
||||
var temp = TestResources.GetQuickTestBeatmapForImport();
|
||||
|
||||
var importedSet = await importer.Import(new ImportTask(temp));
|
||||
|
||||
Assert.NotNull(importedSet);
|
||||
|
||||
ensureLoaded(realm);
|
||||
|
||||
waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
|
||||
|
||||
return realm.All<RealmBeatmapSet>().FirstOrDefault(beatmapSet => beatmapSet.ID == importedSet!.ID);
|
||||
}
|
||||
|
||||
public static async Task<RealmBeatmapSet> LoadOszIntoStore(BeatmapImporter importer, Realm realm, string? path = null, bool virtualTrack = false)
|
||||
{
|
||||
var temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack);
|
||||
|
||||
var importedSet = await importer.Import(new ImportTask(temp));
|
||||
|
||||
Assert.NotNull(importedSet);
|
||||
Debug.Assert(importedSet != null);
|
||||
|
||||
ensureLoaded(realm);
|
||||
|
||||
waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
|
||||
|
||||
return realm.All<RealmBeatmapSet>().First(beatmapSet => beatmapSet.ID == importedSet.ID);
|
||||
}
|
||||
|
||||
private void deleteBeatmapSet(RealmBeatmapSet imported, Realm realm)
|
||||
{
|
||||
realm.Write(() => imported.DeletePending = true);
|
||||
|
||||
checkBeatmapSetCount(realm, 0);
|
||||
checkBeatmapSetCount(realm, 1, true);
|
||||
|
||||
Assert.IsTrue(realm.All<RealmBeatmapSet>().First(_ => true).DeletePending);
|
||||
}
|
||||
|
||||
private static Task createScoreForBeatmap(Realm realm, RealmBeatmap beatmap)
|
||||
{
|
||||
// TODO: reimplement when we have score support in realm.
|
||||
// return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
|
||||
// {
|
||||
// OnlineScoreID = 2,
|
||||
// Beatmap = beatmap,
|
||||
// BeatmapInfoID = beatmap.ID
|
||||
// }, new ImportScoreTest.TestArchiveReader());
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void checkBeatmapSetCount(Realm realm, int expected, bool includeDeletePending = false)
|
||||
{
|
||||
Assert.AreEqual(expected, includeDeletePending
|
||||
? realm.All<RealmBeatmapSet>().Count()
|
||||
: realm.All<RealmBeatmapSet>().Count(s => !s.DeletePending));
|
||||
}
|
||||
|
||||
private static string hashFile(string filename)
|
||||
{
|
||||
using (var s = File.OpenRead(filename))
|
||||
return s.ComputeMD5Hash();
|
||||
}
|
||||
|
||||
private static void checkBeatmapCount(Realm realm, int expected)
|
||||
{
|
||||
Assert.AreEqual(expected, realm.All<RealmBeatmap>().Where(_ => true).ToList().Count);
|
||||
}
|
||||
|
||||
private static void checkSingleReferencedFileCount(Realm realm, int expected)
|
||||
{
|
||||
int singleReferencedCount = 0;
|
||||
|
||||
foreach (var f in realm.All<RealmFile>())
|
||||
{
|
||||
if (f.BacklinksCount == 1)
|
||||
singleReferencedCount++;
|
||||
}
|
||||
|
||||
Assert.AreEqual(expected, singleReferencedCount);
|
||||
}
|
||||
|
||||
private static void ensureLoaded(Realm realm, int timeout = 60000)
|
||||
{
|
||||
IQueryable<RealmBeatmapSet>? resultSets = null;
|
||||
|
||||
waitForOrAssert(() => (resultSets = realm.All<RealmBeatmapSet>().Where(s => !s.DeletePending && s.OnlineID == 241526)).Any(),
|
||||
@"BeatmapSet did not import to the database in allocated time.", timeout);
|
||||
|
||||
// ensure we were stored to beatmap database backing...
|
||||
Assert.IsTrue(resultSets?.Count() == 1, $@"Incorrect result count found ({resultSets?.Count()} but should be 1).");
|
||||
|
||||
IEnumerable<RealmBeatmapSet> queryBeatmapSets() => realm.All<RealmBeatmapSet>().Where(s => !s.DeletePending && s.OnlineID == 241526);
|
||||
|
||||
var set = queryBeatmapSets().First();
|
||||
|
||||
// ReSharper disable once PossibleUnintendedReferenceComparison
|
||||
IEnumerable<RealmBeatmap> queryBeatmaps() => realm.All<RealmBeatmap>().Where(s => s.BeatmapSet != null && s.BeatmapSet == set);
|
||||
|
||||
waitForOrAssert(() => queryBeatmaps().Count() == 12, @"Beatmaps did not import to the database in allocated time", timeout);
|
||||
waitForOrAssert(() => queryBeatmapSets().Count() == 1, @"BeatmapSet did not import to the database in allocated time", timeout);
|
||||
|
||||
int countBeatmapSetBeatmaps = 0;
|
||||
int countBeatmaps = 0;
|
||||
|
||||
waitForOrAssert(() =>
|
||||
(countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count) ==
|
||||
(countBeatmaps = queryBeatmaps().Count()),
|
||||
$@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).", timeout);
|
||||
|
||||
foreach (RealmBeatmap b in set.Beatmaps)
|
||||
Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineID == b.OnlineID));
|
||||
Assert.IsTrue(set.Beatmaps.Count > 0);
|
||||
}
|
||||
|
||||
private static void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 60000)
|
||||
{
|
||||
const int sleep = 200;
|
||||
|
||||
while (timeout > 0)
|
||||
{
|
||||
Thread.Sleep(sleep);
|
||||
timeout -= sleep;
|
||||
|
||||
if (result())
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.Fail(failureMessage);
|
||||
}
|
||||
}
|
||||
}
|
114
osu.Game.Tests/Database/FileStoreTests.cs
Normal file
114
osu.Game.Tests/Database/FileStoreTests.cs
Normal 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));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
67
osu.Game.Tests/Database/GeneralUsageTests.cs
Normal file
67
osu.Game.Tests/Database/GeneralUsageTests.cs
Normal file
@ -0,0 +1,67 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Tests.Database
|
||||
{
|
||||
[TestFixture]
|
||||
public class GeneralUsageTests : RealmTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Just test the construction of a new database works.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestConstructRealm()
|
||||
{
|
||||
RunTestWithRealm((realmFactory, _) => { realmFactory.CreateContext().Refresh(); });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBlockOperations()
|
||||
{
|
||||
RunTestWithRealm((realmFactory, _) =>
|
||||
{
|
||||
using (realmFactory.BlockAllOperations())
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBlockOperationsWithContention()
|
||||
{
|
||||
RunTestWithRealm((realmFactory, _) =>
|
||||
{
|
||||
ManualResetEventSlim stopThreadedUsage = new ManualResetEventSlim();
|
||||
ManualResetEventSlim hasThreadedUsage = new ManualResetEventSlim();
|
||||
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
using (realmFactory.CreateContext())
|
||||
{
|
||||
hasThreadedUsage.Set();
|
||||
|
||||
stopThreadedUsage.Wait();
|
||||
}
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler);
|
||||
|
||||
hasThreadedUsage.Wait();
|
||||
|
||||
Assert.Throws<TimeoutException>(() =>
|
||||
{
|
||||
using (realmFactory.BlockAllOperations())
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
stopThreadedUsage.Set();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
213
osu.Game.Tests/Database/RealmLiveTests.cs
Normal file
213
osu.Game.Tests/Database/RealmLiveTests.cs
Normal 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++;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
151
osu.Game.Tests/Database/RealmTest.cs
Normal file
151
osu.Game.Tests/Database/RealmTest.cs
Normal file
@ -0,0 +1,151 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Models;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Tests.Database
|
||||
{
|
||||
[TestFixture]
|
||||
public abstract class RealmTest
|
||||
{
|
||||
private static readonly TemporaryNativeStorage storage;
|
||||
|
||||
static RealmTest()
|
||||
{
|
||||
storage = new TemporaryNativeStorage("realm-test");
|
||||
storage.DeleteDirectory(string.Empty);
|
||||
}
|
||||
|
||||
protected void RunTestWithRealm(Action<RealmContextFactory, Storage> testAction, [CallerMemberName] string caller = "")
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller))
|
||||
{
|
||||
host.Run(new RealmTestGame(() =>
|
||||
{
|
||||
var testStorage = storage.GetStorageForDirectory(caller);
|
||||
|
||||
using (var realmFactory = new RealmContextFactory(testStorage, caller))
|
||||
{
|
||||
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
|
||||
testAction(realmFactory, testStorage);
|
||||
|
||||
realmFactory.Dispose();
|
||||
|
||||
Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
|
||||
realmFactory.Compact();
|
||||
Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}");
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
protected void RunTestWithRealmAsync(Func<RealmContextFactory, Storage, Task> testAction, [CallerMemberName] string caller = "")
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller))
|
||||
{
|
||||
host.Run(new RealmTestGame(async () =>
|
||||
{
|
||||
var testStorage = storage.GetStorageForDirectory(caller);
|
||||
|
||||
using (var realmFactory = new RealmContextFactory(testStorage, caller))
|
||||
{
|
||||
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
|
||||
await testAction(realmFactory, testStorage);
|
||||
|
||||
realmFactory.Dispose();
|
||||
|
||||
Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
|
||||
realmFactory.Compact();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
protected static RealmBeatmapSet CreateBeatmapSet(RealmRuleset ruleset)
|
||||
{
|
||||
RealmFile createRealmFile() => new RealmFile { Hash = Guid.NewGuid().ToString().ComputeSHA2Hash() };
|
||||
|
||||
var metadata = new RealmBeatmapMetadata
|
||||
{
|
||||
Title = "My Love",
|
||||
Artist = "Kuba Oms"
|
||||
};
|
||||
|
||||
var beatmapSet = new RealmBeatmapSet
|
||||
{
|
||||
Beatmaps =
|
||||
{
|
||||
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Easy", },
|
||||
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Normal", },
|
||||
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Hard", },
|
||||
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Insane", }
|
||||
},
|
||||
Files =
|
||||
{
|
||||
new RealmNamedFileUsage(createRealmFile(), "test [easy].osu"),
|
||||
new RealmNamedFileUsage(createRealmFile(), "test [normal].osu"),
|
||||
new RealmNamedFileUsage(createRealmFile(), "test [hard].osu"),
|
||||
new RealmNamedFileUsage(createRealmFile(), "test [insane].osu"),
|
||||
}
|
||||
};
|
||||
|
||||
for (int i = 0; i < 8; i++)
|
||||
beatmapSet.Files.Add(new RealmNamedFileUsage(createRealmFile(), $"hitsound{i}.mp3"));
|
||||
|
||||
foreach (var b in beatmapSet.Beatmaps)
|
||||
b.BeatmapSet = beatmapSet;
|
||||
|
||||
return beatmapSet;
|
||||
}
|
||||
|
||||
protected static RealmRuleset CreateRuleset() =>
|
||||
new RealmRuleset(0, "osu!", "osu", true);
|
||||
|
||||
private class RealmTestGame : Framework.Game
|
||||
{
|
||||
public RealmTestGame(Func<Task> work)
|
||||
{
|
||||
// ReSharper disable once AsyncVoidLambda
|
||||
Scheduler.Add(async () =>
|
||||
{
|
||||
await work().ConfigureAwait(true);
|
||||
Exit();
|
||||
});
|
||||
}
|
||||
|
||||
public RealmTestGame(Action work)
|
||||
{
|
||||
Scheduler.Add(() =>
|
||||
{
|
||||
work();
|
||||
Exit();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static long getFileSize(Storage testStorage, RealmContextFactory realmFactory)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var stream = testStorage.GetStream(realmFactory.Filename))
|
||||
return stream?.Length ?? 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// windows runs may error due to file still being open.
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
osu.Game.Tests/Database/RulesetStoreTests.cs
Normal file
54
osu.Game.Tests/Database/RulesetStoreTests.cs
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -32,7 +32,7 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
storage = new NativeStorage(directory.FullName);
|
||||
|
||||
realmContextFactory = new RealmContextFactory(storage);
|
||||
realmContextFactory = new RealmContextFactory(storage, "test");
|
||||
keyBindingStore = new RealmKeyBindingStore(realmContextFactory);
|
||||
}
|
||||
|
||||
@ -53,9 +53,9 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
private int queryCount(GlobalAction? match = null)
|
||||
{
|
||||
using (var usage = realmContextFactory.GetForRead())
|
||||
using (var realm = realmContextFactory.CreateContext())
|
||||
{
|
||||
var results = usage.Realm.All<RealmKeyBinding>();
|
||||
var results = realm.All<RealmKeyBinding>();
|
||||
if (match.HasValue)
|
||||
results = results.Where(k => k.ActionInt == (int)match.Value);
|
||||
return results.Count();
|
||||
@ -69,26 +69,24 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>());
|
||||
|
||||
using (var primaryUsage = realmContextFactory.GetForRead())
|
||||
using (var primaryRealm = realmContextFactory.CreateContext())
|
||||
{
|
||||
var backBinding = primaryUsage.Realm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
|
||||
var backBinding = primaryRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
|
||||
|
||||
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
|
||||
|
||||
var tsr = ThreadSafeReference.Create(backBinding);
|
||||
|
||||
using (var usage = realmContextFactory.GetForWrite())
|
||||
using (var threadedContext = realmContextFactory.CreateContext())
|
||||
{
|
||||
var binding = usage.Realm.ResolveReference(tsr);
|
||||
binding.KeyCombination = new KeyCombination(InputKey.BackSpace);
|
||||
|
||||
usage.Commit();
|
||||
var binding = threadedContext.ResolveReference(tsr);
|
||||
threadedContext.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace));
|
||||
}
|
||||
|
||||
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
|
||||
|
||||
// check still correct after re-query.
|
||||
backBinding = primaryUsage.Realm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
|
||||
backBinding = primaryRealm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
|
||||
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
|
||||
}
|
||||
}
|
||||
|
104
osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs
Normal file
104
osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user