mirror of
https://github.com/ppy/osu.git
synced 2025-02-13 22:13:20 +08:00
Merge branch 'master' into new-chat-channel-listing
This commit is contained in:
commit
0155519343
@ -52,10 +52,10 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.304.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.304.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.304.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.308.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Transitive Dependencies">
|
<ItemGroup Label="Transitive Dependencies">
|
||||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||||
<PackageReference Include="Realm" Version="10.9.0" />
|
<PackageReference Include="Realm" Version="10.10.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Clowd.Squirrel" Version="2.8.15-pre" />
|
<PackageReference Include="Clowd.Squirrel" Version="2.8.28-pre" />
|
||||||
<PackageReference Include="Microsoft.NETCore.Targets" Version="5.0.0" />
|
<PackageReference Include="Microsoft.NETCore.Targets" Version="5.0.0" />
|
||||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||||
<PackageReference Include="System.IO.Packaging" Version="6.0.0" />
|
<PackageReference Include="System.IO.Packaging" Version="6.0.0" />
|
||||||
|
@ -7,5 +7,6 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
|||||||
{
|
{
|
||||||
public class CatchScoreProcessor : ScoreProcessor
|
public class CatchScoreProcessor : ScoreProcessor
|
||||||
{
|
{
|
||||||
|
protected override double ClassicScoreMultiplier => 28;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -258,6 +258,7 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
{
|
{
|
||||||
new MultiMod(new ModWindUp(), new ModWindDown()),
|
new MultiMod(new ModWindUp(), new ModWindDown()),
|
||||||
new ManiaModMuted(),
|
new ManiaModMuted(),
|
||||||
|
new ModAdaptiveSpeed()
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -10,5 +10,7 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
|||||||
protected override double DefaultAccuracyPortion => 0.99;
|
protected override double DefaultAccuracyPortion => 0.99;
|
||||||
|
|
||||||
protected override double DefaultComboPortion => 0.01;
|
protected override double DefaultComboPortion => 0.01;
|
||||||
|
|
||||||
|
protected override double ClassicScoreMultiplier => 16;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||||
<PackageReference Include="Moq" Version="4.16.1" />
|
<PackageReference Include="Moq" Version="4.17.2" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
@ -25,13 +28,14 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override string Description => "It never gets boring!";
|
public override string Description => "It never gets boring!";
|
||||||
|
|
||||||
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
|
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
|
||||||
|
private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE / 2;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of previous hitobjects to be shifted together when another object is being moved.
|
/// Number of previous hitobjects to be shifted together when another object is being moved.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int preceding_hitobjects_to_shift = 10;
|
private const int preceding_hitobjects_to_shift = 10;
|
||||||
|
|
||||||
private Random rng;
|
private Random? rng;
|
||||||
|
|
||||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||||
{
|
{
|
||||||
@ -44,28 +48,79 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
rng = new Random((int)Seed.Value);
|
rng = new Random((int)Seed.Value);
|
||||||
|
|
||||||
RandomObjectInfo previous = null;
|
var randomObjects = randomiseObjects(hitObjects);
|
||||||
|
|
||||||
|
applyRandomisation(hitObjects, randomObjects);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Randomise the position of each hit object and return a list of <see cref="RandomObjectInfo"/>s describing how each hit object should be placed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hitObjects">A list of <see cref="OsuHitObject"/>s to have their positions randomised.</param>
|
||||||
|
/// <returns>A list of <see cref="RandomObjectInfo"/>s describing how each hit object should be placed.</returns>
|
||||||
|
private List<RandomObjectInfo> randomiseObjects(IEnumerable<OsuHitObject> hitObjects)
|
||||||
|
{
|
||||||
|
Debug.Assert(rng != null, $"{nameof(ApplyToBeatmap)} was not called before randomising objects");
|
||||||
|
|
||||||
|
var randomObjects = new List<RandomObjectInfo>();
|
||||||
|
RandomObjectInfo? previous = null;
|
||||||
float rateOfChangeMultiplier = 0;
|
float rateOfChangeMultiplier = 0;
|
||||||
|
|
||||||
for (int i = 0; i < hitObjects.Count; i++)
|
foreach (OsuHitObject hitObject in hitObjects)
|
||||||
{
|
{
|
||||||
var hitObject = hitObjects[i];
|
|
||||||
|
|
||||||
var current = new RandomObjectInfo(hitObject);
|
var current = new RandomObjectInfo(hitObject);
|
||||||
|
randomObjects.Add(current);
|
||||||
|
|
||||||
// rateOfChangeMultiplier only changes every 5 iterations in a combo
|
// rateOfChangeMultiplier only changes every 5 iterations in a combo
|
||||||
// to prevent shaky-line-shaped streams
|
// to prevent shaky-line-shaped streams
|
||||||
if (hitObject.IndexInCurrentCombo % 5 == 0)
|
if (hitObject.IndexInCurrentCombo % 5 == 0)
|
||||||
rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1;
|
rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1;
|
||||||
|
|
||||||
|
if (previous == null)
|
||||||
|
{
|
||||||
|
current.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2);
|
||||||
|
current.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
current.DistanceFromPrevious = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal);
|
||||||
|
|
||||||
|
// The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object)
|
||||||
|
// is proportional to the distance between the last and the current hit object
|
||||||
|
// to allow jumps and prevent too sharp turns during streams.
|
||||||
|
|
||||||
|
// Allow maximum jump angle when jump distance is more than half of playfield diagonal length
|
||||||
|
current.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, current.DistanceFromPrevious / (playfield_diagonal * 0.5f));
|
||||||
|
}
|
||||||
|
|
||||||
|
previous = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
return randomObjects;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reposition the hit objects according to the information in <paramref name="randomObjects"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hitObjects">The hit objects to be repositioned.</param>
|
||||||
|
/// <param name="randomObjects">A list of <see cref="RandomObjectInfo"/> describing how each hit object should be placed.</param>
|
||||||
|
private void applyRandomisation(IReadOnlyList<OsuHitObject> hitObjects, IReadOnlyList<RandomObjectInfo> randomObjects)
|
||||||
|
{
|
||||||
|
RandomObjectInfo? previous = null;
|
||||||
|
|
||||||
|
for (int i = 0; i < hitObjects.Count; i++)
|
||||||
|
{
|
||||||
|
var hitObject = hitObjects[i];
|
||||||
|
|
||||||
|
var current = randomObjects[i];
|
||||||
|
|
||||||
if (hitObject is Spinner)
|
if (hitObject is Spinner)
|
||||||
{
|
{
|
||||||
previous = null;
|
previous = null;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyRandomisation(rateOfChangeMultiplier, previous, current);
|
computeRandomisedPosition(current, previous, i > 1 ? randomObjects[i - 2] : null);
|
||||||
|
|
||||||
// Move hit objects back into the playfield if they are outside of it
|
// Move hit objects back into the playfield if they are outside of it
|
||||||
Vector2 shift = Vector2.Zero;
|
Vector2 shift = Vector2.Zero;
|
||||||
@ -102,44 +157,34 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the final position of the hit object
|
/// Compute the randomised position of a hit object while attempting to keep it inside the playfield.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Final position of the hit object</returns>
|
/// <param name="current">The <see cref="RandomObjectInfo"/> representing the hit object to have the randomised position computed for.</param>
|
||||||
private void applyRandomisation(float rateOfChangeMultiplier, RandomObjectInfo previous, RandomObjectInfo current)
|
/// <param name="previous">The <see cref="RandomObjectInfo"/> representing the hit object immediately preceding the current one.</param>
|
||||||
|
/// <param name="beforePrevious">The <see cref="RandomObjectInfo"/> representing the hit object immediately preceding the <paramref name="previous"/> one.</param>
|
||||||
|
private void computeRandomisedPosition(RandomObjectInfo current, RandomObjectInfo? previous, RandomObjectInfo? beforePrevious)
|
||||||
{
|
{
|
||||||
if (previous == null)
|
float previousAbsoluteAngle = 0f;
|
||||||
|
|
||||||
|
if (previous != null)
|
||||||
{
|
{
|
||||||
var playfieldSize = OsuPlayfield.BASE_SIZE;
|
Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
|
||||||
|
Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
|
||||||
current.AngleRad = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
|
previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
|
||||||
current.PositionRandomised = new Vector2((float)rng.NextDouble() * playfieldSize.X, (float)rng.NextDouble() * playfieldSize.Y);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
float distanceToPrev = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal);
|
float absoluteAngle = previousAbsoluteAngle + current.RelativeAngle;
|
||||||
|
|
||||||
// The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object)
|
|
||||||
// is proportional to the distance between the last and the current hit object
|
|
||||||
// to allow jumps and prevent too sharp turns during streams.
|
|
||||||
|
|
||||||
// Allow maximum jump angle when jump distance is more than half of playfield diagonal length
|
|
||||||
double randomAngleRad = rateOfChangeMultiplier * 2 * Math.PI * Math.Min(1f, distanceToPrev / (playfield_diagonal * 0.5f));
|
|
||||||
|
|
||||||
current.AngleRad = (float)randomAngleRad + previous.AngleRad;
|
|
||||||
if (current.AngleRad < 0)
|
|
||||||
current.AngleRad += 2 * (float)Math.PI;
|
|
||||||
|
|
||||||
var posRelativeToPrev = new Vector2(
|
var posRelativeToPrev = new Vector2(
|
||||||
distanceToPrev * (float)Math.Cos(current.AngleRad),
|
current.DistanceFromPrevious * (float)Math.Cos(absoluteAngle),
|
||||||
distanceToPrev * (float)Math.Sin(current.AngleRad)
|
current.DistanceFromPrevious * (float)Math.Sin(absoluteAngle)
|
||||||
);
|
);
|
||||||
|
|
||||||
posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(previous.EndPositionRandomised, posRelativeToPrev);
|
Vector2 lastEndPosition = previous?.EndPositionRandomised ?? playfield_centre;
|
||||||
|
|
||||||
current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X);
|
posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
|
||||||
|
|
||||||
current.PositionRandomised = previous.EndPositionRandomised + posRelativeToPrev;
|
current.PositionRandomised = lastEndPosition + posRelativeToPrev;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -287,7 +332,25 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
private class RandomObjectInfo
|
private class RandomObjectInfo
|
||||||
{
|
{
|
||||||
public float AngleRad { get; set; }
|
/// <summary>
|
||||||
|
/// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <see cref="RelativeAngle"/> of the first hit object in a beatmap represents the absolute angle from playfield center to the object.
|
||||||
|
/// </remarks>
|
||||||
|
/// <example>
|
||||||
|
/// If <see cref="RelativeAngle"/> is 0, the player's cursor doesn't need to change its direction of movement when passing
|
||||||
|
/// the previous object to reach this one.
|
||||||
|
/// </example>
|
||||||
|
public float RelativeAngle { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The jump distance from the previous hit object to this one.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <see cref="DistanceFromPrevious"/> of the first hit object in a beatmap is relative to the playfield center.
|
||||||
|
/// </remarks>
|
||||||
|
public float DistanceFromPrevious { get; set; }
|
||||||
|
|
||||||
public Vector2 PositionOriginal { get; }
|
public Vector2 PositionOriginal { get; }
|
||||||
public Vector2 PositionRandomised { get; set; }
|
public Vector2 PositionRandomised { get; set; }
|
||||||
@ -295,11 +358,13 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public Vector2 EndPositionOriginal { get; }
|
public Vector2 EndPositionOriginal { get; }
|
||||||
public Vector2 EndPositionRandomised { get; set; }
|
public Vector2 EndPositionRandomised { get; set; }
|
||||||
|
|
||||||
|
public OsuHitObject HitObject { get; }
|
||||||
|
|
||||||
public RandomObjectInfo(OsuHitObject hitObject)
|
public RandomObjectInfo(OsuHitObject hitObject)
|
||||||
{
|
{
|
||||||
PositionRandomised = PositionOriginal = hitObject.Position;
|
PositionRandomised = PositionOriginal = hitObject.Position;
|
||||||
EndPositionRandomised = EndPositionOriginal = hitObject.EndPosition;
|
EndPositionRandomised = EndPositionOriginal = hitObject.EndPosition;
|
||||||
AngleRad = 0;
|
HitObject = hitObject;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -195,6 +195,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
new OsuModMuted(),
|
new OsuModMuted(),
|
||||||
new OsuModNoScope(),
|
new OsuModNoScope(),
|
||||||
new OsuModAimAssist(),
|
new OsuModAimAssist(),
|
||||||
|
new ModAdaptiveSpeed()
|
||||||
};
|
};
|
||||||
|
|
||||||
case ModType.System:
|
case ModType.System:
|
||||||
|
@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Osu.Scoring
|
|||||||
{
|
{
|
||||||
public class OsuScoreProcessor : ScoreProcessor
|
public class OsuScoreProcessor : ScoreProcessor
|
||||||
{
|
{
|
||||||
|
protected override double ClassicScoreMultiplier => 36;
|
||||||
|
|
||||||
protected override HitEvent CreateHitEvent(JudgementResult result)
|
protected override HitEvent CreateHitEvent(JudgementResult result)
|
||||||
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
|
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
|
||||||
|
|
||||||
|
@ -10,5 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring
|
|||||||
protected override double DefaultAccuracyPortion => 0.75;
|
protected override double DefaultAccuracyPortion => 0.75;
|
||||||
|
|
||||||
protected override double DefaultComboPortion => 0.25;
|
protected override double DefaultComboPortion => 0.25;
|
||||||
|
|
||||||
|
protected override double ClassicScoreMultiplier => 22;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -151,6 +151,7 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
{
|
{
|
||||||
new MultiMod(new ModWindUp(), new ModWindDown()),
|
new MultiMod(new ModWindUp(), new ModWindDown()),
|
||||||
new TaikoModMuted(),
|
new TaikoModMuted(),
|
||||||
|
new ModAdaptiveSpeed()
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -79,7 +79,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
||||||
<PackageReference Include="Moq" Version="4.16.1" />
|
<PackageReference Include="Moq" Version="4.17.2" />
|
||||||
<PackageReference Include="System.Formats.Asn1">
|
<PackageReference Include="System.Formats.Asn1">
|
||||||
<Version>5.0.0</Version>
|
<Version>5.0.0</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
||||||
<PackageReference Include="Moq" Version="4.16.1" />
|
<PackageReference Include="Moq" Version="4.17.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
|
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -33,16 +33,25 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
|
|
||||||
private TestResultsScreen resultsScreen;
|
private TestResultsScreen resultsScreen;
|
||||||
|
|
||||||
private int currentScoreId;
|
private int lowestScoreId; // Score ID of the lowest score in the list.
|
||||||
|
private int highestScoreId; // Score ID of the highest score in the list.
|
||||||
|
|
||||||
private bool requestComplete;
|
private bool requestComplete;
|
||||||
private int totalCount;
|
private int totalCount;
|
||||||
|
private ScoreInfo userScore;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup() => Schedule(() =>
|
public void Setup() => Schedule(() =>
|
||||||
{
|
{
|
||||||
currentScoreId = 1;
|
lowestScoreId = 1;
|
||||||
|
highestScoreId = 1;
|
||||||
requestComplete = false;
|
requestComplete = false;
|
||||||
totalCount = 0;
|
totalCount = 0;
|
||||||
|
|
||||||
|
userScore = TestResources.CreateTestScoreInfo();
|
||||||
|
userScore.TotalScore = 0;
|
||||||
|
userScore.Statistics = new Dictionary<HitResult, int>();
|
||||||
|
|
||||||
bindHandler();
|
bindHandler();
|
||||||
|
|
||||||
// beatmap is required to be an actual beatmap so the scores can get their scores correctly calculated for standardised scoring.
|
// beatmap is required to be an actual beatmap so the scores can get their scores correctly calculated for standardised scoring.
|
||||||
@ -53,15 +62,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestShowWithUserScore()
|
public void TestShowWithUserScore()
|
||||||
{
|
{
|
||||||
ScoreInfo userScore = null;
|
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
|
||||||
|
|
||||||
AddStep("bind user score info handler", () =>
|
|
||||||
{
|
|
||||||
userScore = TestResources.CreateTestScoreInfo();
|
|
||||||
userScore.OnlineID = currentScoreId++;
|
|
||||||
|
|
||||||
bindHandler(userScore: userScore);
|
|
||||||
});
|
|
||||||
|
|
||||||
createResults(() => userScore);
|
createResults(() => userScore);
|
||||||
|
|
||||||
@ -81,15 +82,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestShowUserScoreWithDelay()
|
public void TestShowUserScoreWithDelay()
|
||||||
{
|
{
|
||||||
ScoreInfo userScore = null;
|
AddStep("bind user score info handler", () => bindHandler(true, userScore));
|
||||||
|
|
||||||
AddStep("bind user score info handler", () =>
|
|
||||||
{
|
|
||||||
userScore = TestResources.CreateTestScoreInfo();
|
|
||||||
userScore.OnlineID = currentScoreId++;
|
|
||||||
|
|
||||||
bindHandler(true, userScore);
|
|
||||||
});
|
|
||||||
|
|
||||||
createResults(() => userScore);
|
createResults(() => userScore);
|
||||||
|
|
||||||
@ -124,7 +117,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
|
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
|
||||||
waitForDisplay();
|
waitForDisplay();
|
||||||
|
|
||||||
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
|
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() >= beforePanelCount + scores_per_result);
|
||||||
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
|
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,15 +125,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestFetchWhenScrolledToTheLeft()
|
public void TestFetchWhenScrolledToTheLeft()
|
||||||
{
|
{
|
||||||
ScoreInfo userScore = null;
|
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
|
||||||
|
|
||||||
AddStep("bind user score info handler", () =>
|
|
||||||
{
|
|
||||||
userScore = TestResources.CreateTestScoreInfo();
|
|
||||||
userScore.OnlineID = currentScoreId++;
|
|
||||||
|
|
||||||
bindHandler(userScore: userScore);
|
|
||||||
});
|
|
||||||
|
|
||||||
createResults(() => userScore);
|
createResults(() => userScore);
|
||||||
|
|
||||||
@ -156,7 +141,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
|
AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
|
||||||
waitForDisplay();
|
waitForDisplay();
|
||||||
|
|
||||||
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
|
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() >= beforePanelCount + scores_per_result);
|
||||||
AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden);
|
AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -245,16 +230,13 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
{
|
{
|
||||||
var multiplayerUserScore = new MultiplayerScore
|
var multiplayerUserScore = new MultiplayerScore
|
||||||
{
|
{
|
||||||
ID = (int)(userScore.OnlineID > 0 ? userScore.OnlineID : currentScoreId++),
|
ID = highestScoreId,
|
||||||
Accuracy = userScore.Accuracy,
|
Accuracy = userScore.Accuracy,
|
||||||
EndedAt = userScore.Date,
|
|
||||||
Passed = userScore.Passed,
|
Passed = userScore.Passed,
|
||||||
Rank = userScore.Rank,
|
Rank = userScore.Rank,
|
||||||
Position = real_user_position,
|
Position = real_user_position,
|
||||||
MaxCombo = userScore.MaxCombo,
|
MaxCombo = userScore.MaxCombo,
|
||||||
TotalScore = userScore.TotalScore,
|
|
||||||
User = userScore.User,
|
User = userScore.User,
|
||||||
Statistics = userScore.Statistics,
|
|
||||||
ScoresAround = new MultiplayerScoresAround
|
ScoresAround = new MultiplayerScoresAround
|
||||||
{
|
{
|
||||||
Higher = new MultiplayerScores(),
|
Higher = new MultiplayerScores(),
|
||||||
@ -268,38 +250,32 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
{
|
{
|
||||||
multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore
|
multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore
|
||||||
{
|
{
|
||||||
ID = currentScoreId++,
|
ID = --highestScoreId,
|
||||||
Accuracy = userScore.Accuracy,
|
Accuracy = userScore.Accuracy,
|
||||||
EndedAt = userScore.Date,
|
|
||||||
Passed = true,
|
Passed = true,
|
||||||
Rank = userScore.Rank,
|
Rank = userScore.Rank,
|
||||||
MaxCombo = userScore.MaxCombo,
|
MaxCombo = userScore.MaxCombo,
|
||||||
TotalScore = userScore.TotalScore - i,
|
|
||||||
User = new APIUser
|
User = new APIUser
|
||||||
{
|
{
|
||||||
Id = 2,
|
Id = 2,
|
||||||
Username = $"peppy{i}",
|
Username = $"peppy{i}",
|
||||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||||
},
|
},
|
||||||
Statistics = userScore.Statistics
|
|
||||||
});
|
});
|
||||||
|
|
||||||
multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore
|
multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore
|
||||||
{
|
{
|
||||||
ID = currentScoreId++,
|
ID = ++lowestScoreId,
|
||||||
Accuracy = userScore.Accuracy,
|
Accuracy = userScore.Accuracy,
|
||||||
EndedAt = userScore.Date,
|
|
||||||
Passed = true,
|
Passed = true,
|
||||||
Rank = userScore.Rank,
|
Rank = userScore.Rank,
|
||||||
MaxCombo = userScore.MaxCombo,
|
MaxCombo = userScore.MaxCombo,
|
||||||
TotalScore = userScore.TotalScore + i,
|
|
||||||
User = new APIUser
|
User = new APIUser
|
||||||
{
|
{
|
||||||
Id = 2,
|
Id = 2,
|
||||||
Username = $"peppy{i}",
|
Username = $"peppy{i}",
|
||||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||||
},
|
},
|
||||||
Statistics = userScore.Statistics
|
|
||||||
});
|
});
|
||||||
|
|
||||||
totalCount += 2;
|
totalCount += 2;
|
||||||
@ -315,33 +291,23 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
{
|
{
|
||||||
var result = new IndexedMultiplayerScores();
|
var result = new IndexedMultiplayerScores();
|
||||||
|
|
||||||
long startTotalScore = req.Cursor?.Properties["total_score"].ToObject<long>() ?? 1000000;
|
|
||||||
string sort = req.IndexParams?.Properties["sort"].ToObject<string>() ?? "score_desc";
|
string sort = req.IndexParams?.Properties["sort"].ToObject<string>() ?? "score_desc";
|
||||||
|
|
||||||
for (int i = 1; i <= scores_per_result; i++)
|
for (int i = 1; i <= scores_per_result; i++)
|
||||||
{
|
{
|
||||||
result.Scores.Add(new MultiplayerScore
|
result.Scores.Add(new MultiplayerScore
|
||||||
{
|
{
|
||||||
ID = currentScoreId++,
|
ID = sort == "score_asc" ? --highestScoreId : ++lowestScoreId,
|
||||||
Accuracy = 1,
|
Accuracy = 1,
|
||||||
EndedAt = DateTimeOffset.Now,
|
|
||||||
Passed = true,
|
Passed = true,
|
||||||
Rank = ScoreRank.X,
|
Rank = ScoreRank.X,
|
||||||
MaxCombo = 1000,
|
MaxCombo = 1000,
|
||||||
TotalScore = startTotalScore + (sort == "score_asc" ? i : -i),
|
|
||||||
User = new APIUser
|
User = new APIUser
|
||||||
{
|
{
|
||||||
Id = 2,
|
Id = 2,
|
||||||
Username = $"peppy{i}",
|
Username = $"peppy{i}",
|
||||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||||
},
|
},
|
||||||
Statistics = new Dictionary<HitResult, int>
|
|
||||||
{
|
|
||||||
{ HitResult.Miss, 1 },
|
|
||||||
{ HitResult.Meh, 50 },
|
|
||||||
{ HitResult.Good, 100 },
|
|
||||||
{ HitResult.Great, 300 }
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
totalCount++;
|
totalCount++;
|
||||||
@ -367,7 +333,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
{
|
{
|
||||||
Properties = new Dictionary<string, JToken>
|
Properties = new Dictionary<string, JToken>
|
||||||
{
|
{
|
||||||
{ "sort", JToken.FromObject(scores.Scores[^1].TotalScore > scores.Scores[^2].TotalScore ? "score_asc" : "score_desc") }
|
{ "sort", JToken.FromObject(scores.Scores[^1].ID > scores.Scores[^2].ID ? "score_asc" : "score_desc") }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,14 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Models;
|
using osu.Game.Models;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Ranking;
|
using osu.Game.Screens.Ranking;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
@ -208,13 +210,19 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
public void TestKeyboardNavigation()
|
public void TestKeyboardNavigation()
|
||||||
{
|
{
|
||||||
var lowestScore = TestResources.CreateTestScoreInfo();
|
var lowestScore = TestResources.CreateTestScoreInfo();
|
||||||
lowestScore.MaxCombo = 100;
|
lowestScore.OnlineID = 3;
|
||||||
|
lowestScore.TotalScore = 0;
|
||||||
|
lowestScore.Statistics = new Dictionary<HitResult, int>();
|
||||||
|
|
||||||
var middleScore = TestResources.CreateTestScoreInfo();
|
var middleScore = TestResources.CreateTestScoreInfo();
|
||||||
middleScore.MaxCombo = 200;
|
middleScore.OnlineID = 2;
|
||||||
|
middleScore.TotalScore = 0;
|
||||||
|
middleScore.Statistics = new Dictionary<HitResult, int>();
|
||||||
|
|
||||||
var highestScore = TestResources.CreateTestScoreInfo();
|
var highestScore = TestResources.CreateTestScoreInfo();
|
||||||
highestScore.MaxCombo = 300;
|
highestScore.OnlineID = 1;
|
||||||
|
highestScore.TotalScore = 0;
|
||||||
|
highestScore.Statistics = new Dictionary<HitResult, int>();
|
||||||
|
|
||||||
createListStep(() => new ScorePanelList());
|
createListStep(() => new ScorePanelList());
|
||||||
|
|
||||||
|
@ -284,14 +284,13 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
public void TestDummy()
|
public void TestDummy()
|
||||||
{
|
{
|
||||||
createSongSelect();
|
createSongSelect();
|
||||||
AddAssert("dummy selected", () => songSelect.CurrentBeatmap == defaultBeatmap);
|
AddUntilStep("dummy selected", () => songSelect.CurrentBeatmap == defaultBeatmap);
|
||||||
|
|
||||||
AddUntilStep("dummy shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap == defaultBeatmap);
|
AddUntilStep("dummy shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap == defaultBeatmap);
|
||||||
|
|
||||||
addManyTestMaps();
|
addManyTestMaps();
|
||||||
AddWaitStep("wait for select", 3);
|
|
||||||
|
|
||||||
AddAssert("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap);
|
AddUntilStep("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -299,9 +298,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
{
|
{
|
||||||
createSongSelect();
|
createSongSelect();
|
||||||
addManyTestMaps();
|
addManyTestMaps();
|
||||||
AddWaitStep("wait for add", 3);
|
|
||||||
|
|
||||||
AddAssert("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap);
|
AddUntilStep("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap);
|
||||||
|
|
||||||
AddStep(@"Sort by Artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist));
|
AddStep(@"Sort by Artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist));
|
||||||
AddStep(@"Sort by Title", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title));
|
AddStep(@"Sort by Title", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title));
|
||||||
@ -571,6 +569,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
createSongSelect();
|
createSongSelect();
|
||||||
|
|
||||||
|
AddUntilStep("wait for selection", () => !Beatmap.IsDefault);
|
||||||
|
|
||||||
AddStep("press ctrl+enter", () =>
|
AddStep("press ctrl+enter", () =>
|
||||||
{
|
{
|
||||||
InputManager.PressKey(Key.ControlLeft);
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
@ -605,6 +605,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
addRulesetImportStep(0);
|
addRulesetImportStep(0);
|
||||||
createSongSelect();
|
createSongSelect();
|
||||||
|
|
||||||
|
AddUntilStep("wait for selection", () => !Beatmap.IsDefault);
|
||||||
|
|
||||||
DrawableCarouselBeatmapSet set = null;
|
DrawableCarouselBeatmapSet set = null;
|
||||||
AddStep("Find the DrawableCarouselBeatmapSet", () =>
|
AddStep("Find the DrawableCarouselBeatmapSet", () =>
|
||||||
{
|
{
|
||||||
@ -844,6 +846,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
createSongSelect();
|
createSongSelect();
|
||||||
|
|
||||||
|
AddUntilStep("wait for selection", () => !Beatmap.IsDefault);
|
||||||
|
|
||||||
AddStep("present score", () =>
|
AddStep("present score", () =>
|
||||||
{
|
{
|
||||||
// this beatmap change should be overridden by the present.
|
// this beatmap change should be overridden by the present.
|
||||||
|
@ -124,7 +124,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("Became present", () => topLocalRank.IsPresent);
|
AddUntilStep("Became present", () => topLocalRank.IsPresent);
|
||||||
AddAssert("Correct rank", () => topLocalRank.Rank == ScoreRank.B);
|
AddUntilStep("Correct rank", () => topLocalRank.Rank == ScoreRank.B);
|
||||||
|
|
||||||
AddStep("Add higher score for current user", () =>
|
AddStep("Add higher score for current user", () =>
|
||||||
{
|
{
|
||||||
@ -137,7 +137,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
scoreManager.Import(testScoreInfo2);
|
scoreManager.Import(testScoreInfo2);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("Correct rank", () => topLocalRank.Rank == ScoreRank.S);
|
AddUntilStep("Correct rank", () => topLocalRank.Rank == ScoreRank.S);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Mods;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class TestSceneDifficultyMultiplierDisplay : OsuTestScene
|
||||||
|
{
|
||||||
|
[Cached]
|
||||||
|
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDifficultyMultiplierDisplay()
|
||||||
|
{
|
||||||
|
DifficultyMultiplierDisplay multiplierDisplay = null;
|
||||||
|
|
||||||
|
AddStep("create content", () => Child = multiplierDisplay = new DifficultyMultiplierDisplay
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("set multiplier below 1", () => multiplierDisplay.Current.Value = 0.5);
|
||||||
|
AddStep("set multiplier to 1", () => multiplierDisplay.Current.Value = 1);
|
||||||
|
AddStep("set multiplier above 1", () => multiplierDisplay.Current.Value = 1.5);
|
||||||
|
|
||||||
|
AddSliderStep("set multiplier", 0, 2, 1d, multiplier =>
|
||||||
|
{
|
||||||
|
if (multiplierDisplay != null)
|
||||||
|
multiplierDisplay.Current.Value = multiplier;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,7 @@
|
|||||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||||
<PackageReference Include="Moq" Version="4.16.1" />
|
<PackageReference Include="Moq" Version="4.17.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
|
@ -105,6 +105,8 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
public Realm Realm => ensureUpdateRealm();
|
public Realm Realm => ensureUpdateRealm();
|
||||||
|
|
||||||
|
private const string realm_extension = @".realm";
|
||||||
|
|
||||||
private Realm ensureUpdateRealm()
|
private Realm ensureUpdateRealm()
|
||||||
{
|
{
|
||||||
if (isSendingNotificationResetEvents)
|
if (isSendingNotificationResetEvents)
|
||||||
@ -149,11 +151,18 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
Filename = filename;
|
Filename = filename;
|
||||||
|
|
||||||
const string realm_extension = @".realm";
|
|
||||||
|
|
||||||
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
|
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
|
||||||
Filename += realm_extension;
|
Filename += realm_extension;
|
||||||
|
|
||||||
|
string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}";
|
||||||
|
|
||||||
|
// Attempt to recover a newer database version if available.
|
||||||
|
if (storage.Exists(newerVersionFilename))
|
||||||
|
{
|
||||||
|
Logger.Log(@"A newer realm database has been found, attempting recovery...", LoggingTarget.Database);
|
||||||
|
attemptRecoverFromFile(newerVersionFilename);
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// This method triggers the first `getRealmInstance` call, which will implicitly run realm migrations and bring the schema up-to-date.
|
// This method triggers the first `getRealmInstance` call, which will implicitly run realm migrations and bring the schema up-to-date.
|
||||||
@ -161,15 +170,78 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made.");
|
// See https://github.com/realm/realm-core/blob/master/src%2Frealm%2Fobject-store%2Fobject_store.cpp#L1016-L1022
|
||||||
|
// This is the best way we can detect a schema version downgrade.
|
||||||
|
if (e.Message.StartsWith(@"Provided schema version", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Logger.Error(e, "Your local database is too new to work with this version of osu!. Please close osu! and install the latest release to recover your data.");
|
||||||
|
|
||||||
|
// If a newer version database already exists, don't backup again. We can presume that the first backup is the one we care about.
|
||||||
|
if (!storage.Exists(newerVersionFilename))
|
||||||
|
CreateBackup(newerVersionFilename);
|
||||||
|
|
||||||
|
storage.Delete(Filename);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made.");
|
||||||
CreateBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}");
|
CreateBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}");
|
||||||
storage.Delete(Filename);
|
storage.Delete(Filename);
|
||||||
|
}
|
||||||
|
|
||||||
cleanupPendingDeletions();
|
cleanupPendingDeletions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void attemptRecoverFromFile(string recoveryFilename)
|
||||||
|
{
|
||||||
|
Logger.Log($@"Performing recovery from {recoveryFilename}", LoggingTarget.Database);
|
||||||
|
|
||||||
|
// First check the user hasn't started to use the database that is in place..
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var realm = Realm.GetInstance(getConfiguration()))
|
||||||
|
{
|
||||||
|
if (realm.All<ScoreInfo>().Any())
|
||||||
|
{
|
||||||
|
Logger.Log(@"Recovery aborted as the existing database has scores set already.", LoggingTarget.Database);
|
||||||
|
Logger.Log(@"To perform recovery, delete client.realm while osu! is not running.", LoggingTarget.Database);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Even if reading the in place database fails, still attempt to recover.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check that the database we are about to attempt recovery can actually be recovered on this version..
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (Realm.GetInstance(getConfiguration(recoveryFilename)))
|
||||||
|
{
|
||||||
|
// Don't need to do anything, just check that opening the realm works correctly.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Logger.Log(@"Recovery aborted as the newer version could not be loaded by this osu! version.", LoggingTarget.Database);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For extra safety, also store the temporarily-used database which we are about to replace.
|
||||||
|
CreateBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_newer_version_before_recovery{realm_extension}");
|
||||||
|
|
||||||
|
storage.Delete(Filename);
|
||||||
|
|
||||||
|
using (var inputStream = storage.GetStream(recoveryFilename))
|
||||||
|
using (var outputStream = storage.GetStream(Filename, FileAccess.Write, FileMode.Create))
|
||||||
|
inputStream.CopyTo(outputStream);
|
||||||
|
|
||||||
|
storage.Delete(recoveryFilename);
|
||||||
|
Logger.Log(@"Recovery complete!", LoggingTarget.Database);
|
||||||
|
}
|
||||||
|
|
||||||
private void cleanupPendingDeletions()
|
private void cleanupPendingDeletions()
|
||||||
{
|
{
|
||||||
using (var realm = getRealmInstance())
|
using (var realm = getRealmInstance())
|
||||||
@ -476,7 +548,7 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private RealmConfiguration getConfiguration()
|
private RealmConfiguration getConfiguration(string? filename = null)
|
||||||
{
|
{
|
||||||
// This is currently the only usage of temporary files at the osu! side.
|
// This is currently the only usage of temporary files at the osu! side.
|
||||||
// If we use the temporary folder in more situations in the future, this should be moved to a higher level (helper method or OsuGameBase).
|
// If we use the temporary folder in more situations in the future, this should be moved to a higher level (helper method or OsuGameBase).
|
||||||
@ -484,7 +556,7 @@ namespace osu.Game.Database
|
|||||||
if (!Directory.Exists(tempPathLocation))
|
if (!Directory.Exists(tempPathLocation))
|
||||||
Directory.CreateDirectory(tempPathLocation);
|
Directory.CreateDirectory(tempPathLocation);
|
||||||
|
|
||||||
return new RealmConfiguration(storage.GetFullPath(Filename, true))
|
return new RealmConfiguration(storage.GetFullPath(filename ?? Filename, true))
|
||||||
{
|
{
|
||||||
SchemaVersion = schema_version,
|
SchemaVersion = schema_version,
|
||||||
MigrationCallback = onMigration,
|
MigrationCallback = onMigration,
|
||||||
|
@ -10,12 +10,11 @@ using osu.Game.IO.Serialization.Converters;
|
|||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Online.Rooms.RoomStatuses;
|
using osu.Game.Online.Rooms.RoomStatuses;
|
||||||
using osu.Game.Utils;
|
|
||||||
|
|
||||||
namespace osu.Game.Online.Rooms
|
namespace osu.Game.Online.Rooms
|
||||||
{
|
{
|
||||||
[JsonObject(MemberSerialization.OptIn)]
|
[JsonObject(MemberSerialization.OptIn)]
|
||||||
public class Room : IDeepCloneable<Room>
|
public class Room
|
||||||
{
|
{
|
||||||
[Cached]
|
[Cached]
|
||||||
[JsonProperty("id")]
|
[JsonProperty("id")]
|
||||||
@ -153,22 +152,6 @@ namespace osu.Game.Online.Rooms
|
|||||||
Password.BindValueChanged(p => HasPassword.Value = !string.IsNullOrEmpty(p.NewValue));
|
Password.BindValueChanged(p => HasPassword.Value = !string.IsNullOrEmpty(p.NewValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a copy of this room without online information.
|
|
||||||
/// Should be used to create a local copy of a room for submitting in the future.
|
|
||||||
/// </summary>
|
|
||||||
public Room DeepClone()
|
|
||||||
{
|
|
||||||
var copy = new Room();
|
|
||||||
|
|
||||||
copy.CopyFrom(this);
|
|
||||||
|
|
||||||
// ID must be unset as we use this as a marker for whether this is a client-side (not-yet-created) room or not.
|
|
||||||
copy.RoomID.Value = null;
|
|
||||||
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CopyFrom(Room other)
|
public void CopyFrom(Room other)
|
||||||
{
|
{
|
||||||
RoomID.Value = other.RoomID.Value;
|
RoomID.Value = other.RoomID.Value;
|
||||||
|
@ -79,7 +79,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
|||||||
var beatmapInfo = new BeatmapInfo
|
var beatmapInfo = new BeatmapInfo
|
||||||
{
|
{
|
||||||
MaxCombo = apiBeatmap.MaxCombo,
|
MaxCombo = apiBeatmap.MaxCombo,
|
||||||
Status = apiBeatmap.Status
|
Status = apiBeatmap.Status,
|
||||||
|
MD5Hash = apiBeatmap.MD5Hash
|
||||||
};
|
};
|
||||||
|
|
||||||
scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.CreateScoreInfo(rulesets, beatmapInfo)).ToArray(), loadCancellationSource.Token)
|
scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.CreateScoreInfo(rulesets, beatmapInfo)).ToArray(), loadCancellationSource.Token)
|
||||||
|
185
osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs
Normal file
185
osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.LocalisationExtensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Overlays.Mods
|
||||||
|
{
|
||||||
|
public class DifficultyMultiplierDisplay : CompositeDrawable, IHasCurrentValue<double>
|
||||||
|
{
|
||||||
|
public Bindable<double> Current
|
||||||
|
{
|
||||||
|
get => current.Current;
|
||||||
|
set => current.Current = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly BindableNumberWithCurrent<double> current = new BindableNumberWithCurrent<double>(1)
|
||||||
|
{
|
||||||
|
Precision = 0.01
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly Box underlayBackground;
|
||||||
|
private readonly Box contentBackground;
|
||||||
|
private readonly FillFlowContainer multiplierFlow;
|
||||||
|
private readonly MultiplierCounter multiplierCounter;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuColour colours { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OverlayColourProvider colourProvider { get; set; }
|
||||||
|
|
||||||
|
private const float height = 42;
|
||||||
|
private const float multiplier_value_area_width = 56;
|
||||||
|
private const float transition_duration = 200;
|
||||||
|
|
||||||
|
public DifficultyMultiplierDisplay()
|
||||||
|
{
|
||||||
|
Height = height;
|
||||||
|
AutoSizeAxes = Axes.X;
|
||||||
|
|
||||||
|
InternalChild = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
AutoSizeAxes = Axes.X,
|
||||||
|
Masking = true,
|
||||||
|
CornerRadius = ModPanel.CORNER_RADIUS,
|
||||||
|
Shear = new Vector2(ModPanel.SHEAR_X, 0),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
underlayBackground = new Box
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
Width = multiplier_value_area_width + ModPanel.CORNER_RADIUS
|
||||||
|
},
|
||||||
|
new GridContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
AutoSizeAxes = Axes.X,
|
||||||
|
ColumnDimensions = new[]
|
||||||
|
{
|
||||||
|
new Dimension(GridSizeMode.AutoSize),
|
||||||
|
new Dimension(GridSizeMode.Absolute, multiplier_value_area_width)
|
||||||
|
},
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
AutoSizeAxes = Axes.X,
|
||||||
|
Masking = true,
|
||||||
|
CornerRadius = ModPanel.CORNER_RADIUS,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
contentBackground = new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
},
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Margin = new MarginPadding { Horizontal = 18 },
|
||||||
|
Shear = new Vector2(-ModPanel.SHEAR_X, 0),
|
||||||
|
Text = "Difficulty Multiplier",
|
||||||
|
Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
multiplierFlow = new FillFlowContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Shear = new Vector2(-ModPanel.SHEAR_X, 0),
|
||||||
|
Direction = FillDirection.Horizontal,
|
||||||
|
Spacing = new Vector2(2, 0),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
multiplierCounter = new MultiplierCounter
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Current = { BindTarget = Current }
|
||||||
|
},
|
||||||
|
new SpriteIcon
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Icon = FontAwesome.Solid.Times,
|
||||||
|
Size = new Vector2(7),
|
||||||
|
Margin = new MarginPadding { Top = 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
contentBackground.Colour = colourProvider.Background4;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
current.BindValueChanged(_ => updateState(), true);
|
||||||
|
FinishTransforms(true);
|
||||||
|
// required to prevent the counter initially rolling up from 0 to 1
|
||||||
|
// due to `Current.Value` having a nonstandard default value of 1.
|
||||||
|
multiplierCounter.SetCountWithoutRolling(Current.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateState()
|
||||||
|
{
|
||||||
|
if (Current.IsDefault)
|
||||||
|
{
|
||||||
|
underlayBackground.FadeColour(colourProvider.Background3, transition_duration, Easing.OutQuint);
|
||||||
|
multiplierFlow.FadeColour(Colour4.White, transition_duration, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var backgroundColour = Current.Value < 1
|
||||||
|
? colours.ForModType(ModType.DifficultyReduction)
|
||||||
|
: colours.ForModType(ModType.DifficultyIncrease);
|
||||||
|
|
||||||
|
underlayBackground.FadeColour(backgroundColour, transition_duration, Easing.OutQuint);
|
||||||
|
multiplierFlow.FadeColour(colourProvider.Background5, transition_duration, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MultiplierCounter : RollingCounter<double>
|
||||||
|
{
|
||||||
|
protected override double RollingDuration => 500;
|
||||||
|
|
||||||
|
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(@"N2");
|
||||||
|
|
||||||
|
protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText
|
||||||
|
{
|
||||||
|
Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
269
osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs
Normal file
269
osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Audio.Track;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics.Audio;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mods
|
||||||
|
{
|
||||||
|
public class ModAdaptiveSpeed : Mod, IApplicableToRate, IApplicableToDrawableHitObject, IApplicableToBeatmap, IUpdatableByPlayfield
|
||||||
|
{
|
||||||
|
public override string Name => "Adaptive Speed";
|
||||||
|
|
||||||
|
public override string Acronym => "AS";
|
||||||
|
|
||||||
|
public override string Description => "Let track speed adapt to you.";
|
||||||
|
|
||||||
|
public override ModType Type => ModType.Fun;
|
||||||
|
|
||||||
|
public override double ScoreMultiplier => 1;
|
||||||
|
|
||||||
|
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp) };
|
||||||
|
|
||||||
|
[SettingSource("Initial rate", "The starting speed of the track")]
|
||||||
|
public BindableNumber<double> InitialRate { get; } = new BindableDouble
|
||||||
|
{
|
||||||
|
MinValue = 0.5,
|
||||||
|
MaxValue = 2,
|
||||||
|
Default = 1,
|
||||||
|
Value = 1,
|
||||||
|
Precision = 0.01
|
||||||
|
};
|
||||||
|
|
||||||
|
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
|
||||||
|
public BindableBool AdjustPitch { get; } = new BindableBool
|
||||||
|
{
|
||||||
|
Default = true,
|
||||||
|
Value = true
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The instantaneous rate of the track.
|
||||||
|
/// Every frame this mod will attempt to smoothly adjust this to meet <see cref="targetRate"/>.
|
||||||
|
/// </summary>
|
||||||
|
public BindableNumber<double> SpeedChange { get; } = new BindableDouble
|
||||||
|
{
|
||||||
|
MinValue = min_allowable_rate,
|
||||||
|
MaxValue = max_allowable_rate,
|
||||||
|
Default = 1,
|
||||||
|
Value = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
// The two constants below denote the maximum allowable range of rates that `SpeedChange` can take.
|
||||||
|
// The range is purposefully wider than the range of values that `InitialRate` allows
|
||||||
|
// in order to give some leeway for change even when extreme initial rates are chosen.
|
||||||
|
private const double min_allowable_rate = 0.4d;
|
||||||
|
private const double max_allowable_rate = 2.5d;
|
||||||
|
|
||||||
|
// The two constants below denote the maximum allowable change in rate caused by a single hit
|
||||||
|
// This prevents sudden jolts caused by a badly-timed hit.
|
||||||
|
private const double min_allowable_rate_change = 0.9d;
|
||||||
|
private const double max_allowable_rate_change = 1.11d;
|
||||||
|
|
||||||
|
// Apply a fixed rate change when missing, allowing the player to catch up when the rate is too fast.
|
||||||
|
private const double rate_change_on_miss = 0.95d;
|
||||||
|
|
||||||
|
private ITrack track;
|
||||||
|
private double targetRate = 1d;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of most recent track rates (approximated from how early/late each object was hit relative to the previous object)
|
||||||
|
/// which should be averaged to calculate <see cref="targetRate"/>.
|
||||||
|
/// </summary>
|
||||||
|
private const int recent_rate_count = 8;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the most recent <see cref="recent_rate_count"/> approximated track rates
|
||||||
|
/// which are averaged to calculate the value of <see cref="targetRate"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This list is used as a double-ended queue with fixed capacity
|
||||||
|
/// (items can be enqueued/dequeued at either end of the list).
|
||||||
|
/// When time is elapsing forward, items are dequeued from the start and enqueued onto the end of the list.
|
||||||
|
/// When time is being rewound, items are dequeued from the end and enqueued onto the start of the list.
|
||||||
|
/// </remarks>
|
||||||
|
/// <example>
|
||||||
|
/// <para>
|
||||||
|
/// The track rate approximation is calculated as follows:
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Consider a hitobject which ends at 1000ms, and assume that its preceding hitobject ends at 500ms.
|
||||||
|
/// This gives a time difference of 1000 - 500 = 500ms.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Now assume that the user hit this object at 980ms rather than 1000ms.
|
||||||
|
/// When compared to the preceding hitobject, this gives 980 - 500 = 480ms.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// With the above assumptions, the player is rushing / hitting early, which means that the track should speed up to match.
|
||||||
|
/// Therefore, the approximated target rate for this object would be equal to 500 / 480 * <see cref="InitialRate"/>.
|
||||||
|
/// </para>
|
||||||
|
/// </example>
|
||||||
|
private readonly List<double> recentRates = Enumerable.Repeat(1d, recent_rate_count).ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For each given <see cref="HitObject"/> in the map, this dictionary maps the object onto the latest end time of any other object
|
||||||
|
/// that precedes the end time of the given object.
|
||||||
|
/// This can be loosely interpreted as the end time of the preceding hit object in rulesets that do not have overlapping hit objects.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<HitObject, double> precedingEndTimes = new Dictionary<HitObject, double>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For each given <see cref="HitObject"/> in the map, this dictionary maps the object onto the track rate dequeued from
|
||||||
|
/// <see cref="recentRates"/> (i.e. the oldest value in the queue) when the object is hit. If the hit is then reverted,
|
||||||
|
/// the mapped value can be re-introduced to <see cref="recentRates"/> to properly rewind the queue.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<HitObject, double> ratesForRewinding = new Dictionary<HitObject, double>();
|
||||||
|
|
||||||
|
public ModAdaptiveSpeed()
|
||||||
|
{
|
||||||
|
InitialRate.BindValueChanged(val =>
|
||||||
|
{
|
||||||
|
SpeedChange.Value = val.NewValue;
|
||||||
|
targetRate = val.NewValue;
|
||||||
|
});
|
||||||
|
AdjustPitch.BindValueChanged(adjustPitchChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyToTrack(ITrack track)
|
||||||
|
{
|
||||||
|
this.track = track;
|
||||||
|
|
||||||
|
InitialRate.TriggerChange();
|
||||||
|
AdjustPitch.TriggerChange();
|
||||||
|
recentRates.Clear();
|
||||||
|
recentRates.AddRange(Enumerable.Repeat(InitialRate.Value, recent_rate_count));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyToSample(DrawableSample sample)
|
||||||
|
{
|
||||||
|
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update(Playfield playfield)
|
||||||
|
{
|
||||||
|
SpeedChange.Value = Interpolation.DampContinuously(SpeedChange.Value, targetRate, 50, playfield.Clock.ElapsedFrameTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double ApplyToRate(double time, double rate = 1) => rate * InitialRate.Value;
|
||||||
|
|
||||||
|
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
|
||||||
|
{
|
||||||
|
drawable.OnNewResult += (o, result) =>
|
||||||
|
{
|
||||||
|
if (ratesForRewinding.ContainsKey(result.HitObject)) return;
|
||||||
|
if (!shouldProcessResult(result)) return;
|
||||||
|
|
||||||
|
ratesForRewinding.Add(result.HitObject, recentRates[0]);
|
||||||
|
recentRates.RemoveAt(0);
|
||||||
|
|
||||||
|
recentRates.Add(Math.Clamp(getRelativeRateChange(result) * SpeedChange.Value, min_allowable_rate, max_allowable_rate));
|
||||||
|
|
||||||
|
updateTargetRate();
|
||||||
|
};
|
||||||
|
drawable.OnRevertResult += (o, result) =>
|
||||||
|
{
|
||||||
|
if (!ratesForRewinding.ContainsKey(result.HitObject)) return;
|
||||||
|
if (!shouldProcessResult(result)) return;
|
||||||
|
|
||||||
|
recentRates.Insert(0, ratesForRewinding[result.HitObject]);
|
||||||
|
ratesForRewinding.Remove(result.HitObject);
|
||||||
|
|
||||||
|
recentRates.RemoveAt(recentRates.Count - 1);
|
||||||
|
|
||||||
|
updateTargetRate();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||||
|
{
|
||||||
|
var hitObjects = getAllApplicableHitObjects(beatmap.HitObjects).ToList();
|
||||||
|
var endTimes = hitObjects.Select(x => x.GetEndTime()).OrderBy(x => x).Distinct().ToList();
|
||||||
|
|
||||||
|
foreach (HitObject hitObject in hitObjects)
|
||||||
|
{
|
||||||
|
int index = endTimes.BinarySearch(hitObject.GetEndTime());
|
||||||
|
if (index < 0) index = ~index; // BinarySearch returns the next larger element in bitwise complement if there's no exact match
|
||||||
|
index -= 1;
|
||||||
|
|
||||||
|
if (index >= 0)
|
||||||
|
precedingEndTimes.Add(hitObject, endTimes[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void adjustPitchChanged(ValueChangedEvent<bool> adjustPitchSetting)
|
||||||
|
{
|
||||||
|
track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange);
|
||||||
|
|
||||||
|
track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue)
|
||||||
|
=> adjustPitchSettingValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo;
|
||||||
|
|
||||||
|
private IEnumerable<HitObject> getAllApplicableHitObjects(IEnumerable<HitObject> hitObjects)
|
||||||
|
{
|
||||||
|
foreach (var hitObject in hitObjects)
|
||||||
|
{
|
||||||
|
if (!(hitObject.HitWindows is HitWindows.EmptyHitWindows))
|
||||||
|
yield return hitObject;
|
||||||
|
|
||||||
|
foreach (HitObject nested in getAllApplicableHitObjects(hitObject.NestedHitObjects))
|
||||||
|
yield return nested;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool shouldProcessResult(JudgementResult result)
|
||||||
|
{
|
||||||
|
if (!result.Type.AffectsAccuracy()) return false;
|
||||||
|
if (!precedingEndTimes.ContainsKey(result.HitObject)) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double getRelativeRateChange(JudgementResult result)
|
||||||
|
{
|
||||||
|
if (!result.IsHit)
|
||||||
|
return rate_change_on_miss;
|
||||||
|
|
||||||
|
double prevEndTime = precedingEndTimes[result.HitObject];
|
||||||
|
return Math.Clamp(
|
||||||
|
(result.HitObject.GetEndTime() - prevEndTime) / (result.TimeAbsolute - prevEndTime),
|
||||||
|
min_allowable_rate_change,
|
||||||
|
max_allowable_rate_change
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update <see cref="targetRate"/> based on the values in <see cref="recentRates"/>.
|
||||||
|
/// </summary>
|
||||||
|
private void updateTargetRate()
|
||||||
|
{
|
||||||
|
// Compare values in recentRates to see how consistent the player's speed is
|
||||||
|
// If the player hits half of the notes too fast and the other half too slow: Abs(consistency) = 0
|
||||||
|
// If the player hits all their notes too fast or too slow: Abs(consistency) = recent_rate_count - 1
|
||||||
|
int consistency = 0;
|
||||||
|
|
||||||
|
for (int i = 1; i < recentRates.Count; i++)
|
||||||
|
{
|
||||||
|
consistency += Math.Sign(recentRates[i] - recentRates[i - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale the rate adjustment based on consistency
|
||||||
|
targetRate = Interpolation.Lerp(targetRate, recentRates.Average(), Math.Abs(consistency) / (recent_rate_count - 1d));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
|
|
||||||
public double ApplyToRate(double time, double rate) => rate * SpeedChange.Value;
|
public double ApplyToRate(double time, double rate) => rate * SpeedChange.Value;
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp) };
|
public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp), typeof(ModAdaptiveSpeed) };
|
||||||
|
|
||||||
public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x";
|
public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x";
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
|
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
|
||||||
public abstract BindableBool AdjustPitch { get; }
|
public abstract BindableBool AdjustPitch { get; }
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust) };
|
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) };
|
||||||
|
|
||||||
public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x";
|
public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x";
|
||||||
|
|
||||||
|
@ -76,6 +76,11 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual double DefaultComboPortion => 0.7;
|
protected virtual double DefaultComboPortion => 0.7;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An arbitrary multiplier to scale scores in the <see cref="ScoringMode.Classic"/> scoring mode.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual double ClassicScoreMultiplier => 36;
|
||||||
|
|
||||||
private readonly double accuracyPortion;
|
private readonly double accuracyPortion;
|
||||||
private readonly double comboPortion;
|
private readonly double comboPortion;
|
||||||
|
|
||||||
@ -246,7 +251,7 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
// This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring.
|
// This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring.
|
||||||
// The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes.
|
// The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes.
|
||||||
double scaledStandardised = GetScore(ScoringMode.Standardised, accuracyRatio, comboRatio, statistics) / max_score;
|
double scaledStandardised = GetScore(ScoringMode.Standardised, accuracyRatio, comboRatio, statistics) / max_score;
|
||||||
return Math.Pow(scaledStandardised * totalHitObjects, 2) * 36;
|
return Math.Pow(scaledStandardised * totalHitObjects, 2) * ClassicScoreMultiplier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@ namespace osu.Game.Scoring
|
|||||||
public async Task<long> GetTotalScoreAsync([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default)
|
public async Task<long> GetTotalScoreAsync([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
// TODO: This is required for playlist aggregate scores. They should likely not be getting here in the first place.
|
// TODO: This is required for playlist aggregate scores. They should likely not be getting here in the first place.
|
||||||
if (string.IsNullOrEmpty(score.BeatmapInfo.Hash))
|
if (string.IsNullOrEmpty(score.BeatmapInfo.MD5Hash))
|
||||||
return score.TotalScore;
|
return score.TotalScore;
|
||||||
|
|
||||||
int beatmapMaxCombo;
|
int beatmapMaxCombo;
|
||||||
|
@ -128,7 +128,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
|
|||||||
{
|
{
|
||||||
new OsuMenuItem("Create copy", MenuItemType.Standard, () =>
|
new OsuMenuItem("Create copy", MenuItemType.Standard, () =>
|
||||||
{
|
{
|
||||||
lounge?.Open(Room.DeepClone());
|
lounge?.OpenCopy(Room);
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ using osu.Framework.Threading;
|
|||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Input;
|
using osu.Game.Input;
|
||||||
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
@ -63,6 +64,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private IBindable<RulesetInfo> ruleset { get; set; }
|
private IBindable<RulesetInfo> ruleset { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IAPIProvider api { get; set; }
|
||||||
|
|
||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
private IDisposable joiningRoomOperation { get; set; }
|
private IDisposable joiningRoomOperation { get; set; }
|
||||||
|
|
||||||
@ -310,6 +314,42 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copies a room and opens it as a fresh (not-yet-created) one.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="room">The room to copy.</param>
|
||||||
|
public void OpenCopy(Room room)
|
||||||
|
{
|
||||||
|
Debug.Assert(room.RoomID.Value != null);
|
||||||
|
|
||||||
|
if (joiningRoomOperation != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
joiningRoomOperation = ongoingOperationTracker?.BeginOperation();
|
||||||
|
|
||||||
|
var req = new GetRoomRequest(room.RoomID.Value.Value);
|
||||||
|
|
||||||
|
req.Success += r =>
|
||||||
|
{
|
||||||
|
// ID must be unset as we use this as a marker for whether this is a client-side (not-yet-created) room or not.
|
||||||
|
r.RoomID.Value = null;
|
||||||
|
|
||||||
|
Open(r);
|
||||||
|
|
||||||
|
joiningRoomOperation?.Dispose();
|
||||||
|
joiningRoomOperation = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
req.Failure += exception =>
|
||||||
|
{
|
||||||
|
Logger.Error(exception, "Couldn't create a copy of this room.");
|
||||||
|
joiningRoomOperation?.Dispose();
|
||||||
|
joiningRoomOperation = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
api.Queue(req);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Push a room as a new subscreen.
|
/// Push a room as a new subscreen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -17,6 +17,9 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
|
|
||||||
public override bool IsPresent => base.IsPresent || Item?.Visible == true;
|
public override bool IsPresent => base.IsPresent || Item?.Visible == true;
|
||||||
|
|
||||||
|
public override bool HandlePositionalInput => Item?.Visible == true;
|
||||||
|
public override bool PropagatePositionalInputSubTree => Item?.Visible == true;
|
||||||
|
|
||||||
public readonly CarouselHeader Header;
|
public readonly CarouselHeader Header;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -64,7 +64,7 @@ namespace osu.Game.Users.Drawables
|
|||||||
|
|
||||||
private void openProfile()
|
private void openProfile()
|
||||||
{
|
{
|
||||||
if (user?.Id > 1)
|
if (user?.Id > 1 || !string.IsNullOrEmpty(user?.Username))
|
||||||
game?.ShowUser(user);
|
game?.ShowUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,13 +19,13 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="AutoMapper" Version="11.0.1" />
|
<PackageReference Include="AutoMapper" Version="11.0.1" />
|
||||||
<PackageReference Include="DiffPlex" Version="1.7.0" />
|
<PackageReference Include="DiffPlex" Version="1.7.1" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.42" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.42" />
|
||||||
<PackageReference Include="Humanizer" Version="2.14.1" />
|
<PackageReference Include="Humanizer" Version="2.14.1" />
|
||||||
<PackageReference Include="MessagePack" Version="2.3.85" />
|
<PackageReference Include="MessagePack" Version="2.3.85" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.3" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="6.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="6.0.3" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="6.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="6.0.3" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||||
@ -35,10 +35,10 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Realm" Version="10.9.0" />
|
<PackageReference Include="Realm" Version="10.10.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2022.304.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2022.308.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.304.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.304.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.14.0" />
|
<PackageReference Include="Sentry" Version="3.14.1" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.30.1" />
|
<PackageReference Include="SharpCompress" Version="0.30.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
<Reference Include="System.Net.Http" />
|
<Reference Include="System.Net.Http" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.304.0" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.308.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.304.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.304.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
|
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
|
||||||
@ -79,16 +79,16 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
|
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
|
||||||
<ItemGroup Label="Transitive Dependencies">
|
<ItemGroup Label="Transitive Dependencies">
|
||||||
<PackageReference Include="DiffPlex" Version="1.7.0" />
|
<PackageReference Include="DiffPlex" Version="1.7.1" />
|
||||||
<PackageReference Include="Humanizer" Version="2.14.1" />
|
<PackageReference Include="Humanizer" Version="2.14.1" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2022.304.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2022.308.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.30.1" />
|
<PackageReference Include="SharpCompress" Version="0.30.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2021.1221.0" ExcludeAssets="all" />
|
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2021.1221.0" ExcludeAssets="all" />
|
||||||
<PackageReference Include="Realm" Version="10.9.0" />
|
<PackageReference Include="Realm" Version="10.10.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
Loading…
Reference in New Issue
Block a user