mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 17:43:05 +08:00
Merge branch 'master' into arod_rate_adjust
This commit is contained in:
commit
0259ab761b
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1201.1" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1213.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash);
|
||||
|
||||
for (int i = 0; i < 9; i++)
|
||||
for (int i = 0; i < 11; i++)
|
||||
{
|
||||
int count = i + 1;
|
||||
AddUntilStep($"wait for hyperdash #{count}", () => hyperDashCount >= count);
|
||||
@ -104,12 +104,22 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
})
|
||||
}, 1);
|
||||
|
||||
createObjects(() => new Fruit { X = right_x }, count: 2, spacing: 0, spacingAfterGroup: 400);
|
||||
createObjects(() => new TestJuiceStream(left_x)
|
||||
{
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(Vector2.Zero),
|
||||
new PathControlPoint(new Vector2(0, 300))
|
||||
})
|
||||
}, count: 1, spacingAfterGroup: 150);
|
||||
createObjects(() => new Fruit { X = left_x }, count: 1, spacing: 0, spacingAfterGroup: 400);
|
||||
createObjects(() => new Fruit { X = right_x }, count: 2, spacing: 0);
|
||||
|
||||
return beatmap;
|
||||
|
||||
void createObjects(Func<CatchHitObject> createObject, int count = 3)
|
||||
void createObjects(Func<CatchHitObject> createObject, int count = 3, float spacing = 140, float spacingAfterGroup = 700)
|
||||
{
|
||||
const float spacing = 140;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var hitObject = createObject();
|
||||
@ -117,7 +127,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
beatmap.HitObjects.Add(hitObject);
|
||||
}
|
||||
|
||||
startTime += 700;
|
||||
startTime += spacingAfterGroup;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
{
|
||||
@ -38,5 +39,25 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerate all <see cref="PalpableCatchHitObject"/>s, sorted by their start times.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If multiple objects have the same start time, the ordering is preserved (it is a stable sorting).
|
||||
/// </remarks>
|
||||
public static IEnumerable<PalpableCatchHitObject> GetPalpableObjects(IEnumerable<HitObject> hitObjects)
|
||||
{
|
||||
return hitObjects.SelectMany(selectPalpableObjects).OrderBy(h => h.StartTime);
|
||||
|
||||
IEnumerable<PalpableCatchHitObject> selectPalpableObjects(HitObject h)
|
||||
{
|
||||
if (h is PalpableCatchHitObject palpable)
|
||||
yield return palpable;
|
||||
|
||||
foreach (var nested in h.NestedHitObjects.OfType<PalpableCatchHitObject>())
|
||||
yield return nested;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
@ -208,24 +207,9 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
|
||||
private static void initialiseHyperDash(IBeatmap beatmap)
|
||||
{
|
||||
List<PalpableCatchHitObject> palpableObjects = new List<PalpableCatchHitObject>();
|
||||
|
||||
foreach (var currentObject in beatmap.HitObjects)
|
||||
{
|
||||
if (currentObject is Fruit fruitObject)
|
||||
palpableObjects.Add(fruitObject);
|
||||
|
||||
if (currentObject is JuiceStream)
|
||||
{
|
||||
foreach (var juice in currentObject.NestedHitObjects)
|
||||
{
|
||||
if (juice is PalpableCatchHitObject palpableObject && !(juice is TinyDroplet))
|
||||
palpableObjects.Add(palpableObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
|
||||
var palpableObjects = CatchBeatmap.GetPalpableObjects(beatmap.HitObjects)
|
||||
.Where(h => h is Fruit || (h is Droplet && h is not TinyDroplet))
|
||||
.ToArray();
|
||||
|
||||
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) / 2;
|
||||
|
||||
@ -237,7 +221,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
int lastDirection = 0;
|
||||
double lastExcess = halfCatcherWidth;
|
||||
|
||||
for (int i = 0; i < palpableObjects.Count - 1; i++)
|
||||
for (int i = 0; i < palpableObjects.Length - 1; i++)
|
||||
{
|
||||
var currentObject = palpableObjects[i];
|
||||
var nextObject = palpableObjects[i + 1];
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Catch.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
@ -56,13 +57,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
||||
|
||||
// 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.AsEnumerable() : new[] { obj })
|
||||
.Cast<CatchHitObject>()
|
||||
.OrderBy(x => x.StartTime))
|
||||
foreach (var hitObject in CatchBeatmap.GetPalpableObjects(beatmap.HitObjects))
|
||||
{
|
||||
// We want to only consider fruits that contribute to the combo.
|
||||
if (hitObject is BananaShower || hitObject is TinyDroplet)
|
||||
if (hitObject is Banana || hitObject is TinyDroplet)
|
||||
continue;
|
||||
|
||||
if (lastObject != null)
|
||||
|
@ -74,6 +74,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
simulateHit(obj, ref attributes);
|
||||
|
||||
attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore;
|
||||
attributes.BonusScore = legacyBonusScore;
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
@ -126,6 +126,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
|
||||
|
||||
private double? lastHyperDashStartTime;
|
||||
private double hyperDashModifier = 1;
|
||||
private int hyperDashDirection;
|
||||
private float hyperDashTargetPosition;
|
||||
@ -233,16 +234,23 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
// droplet doesn't affect the catcher state
|
||||
if (hitObject is TinyDroplet) return;
|
||||
|
||||
if (result.IsHit && hitObject.HyperDashTarget is CatchHitObject target)
|
||||
// if a hyper fruit was already handled this frame, just go where it says to go.
|
||||
// this special-cases some aspire maps that have doubled-up objects (one hyper, one not) at the same time instant.
|
||||
// handling this "properly" elsewhere is impossible as there is no feasible way to ensure
|
||||
// that the hyperfruit gets judged second (especially if it coincides with a last fruit in a juice stream).
|
||||
if (lastHyperDashStartTime != Time.Current)
|
||||
{
|
||||
double timeDifference = target.StartTime - hitObject.StartTime;
|
||||
double positionDifference = target.EffectiveX - X;
|
||||
double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
|
||||
if (result.IsHit && hitObject.HyperDashTarget is CatchHitObject target)
|
||||
{
|
||||
double timeDifference = target.StartTime - hitObject.StartTime;
|
||||
double positionDifference = target.EffectiveX - X;
|
||||
double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
|
||||
|
||||
SetHyperDashState(Math.Abs(velocity) / BASE_DASH_SPEED, target.EffectiveX);
|
||||
SetHyperDashState(Math.Abs(velocity) / BASE_DASH_SPEED, target.EffectiveX);
|
||||
}
|
||||
else
|
||||
SetHyperDashState();
|
||||
}
|
||||
else
|
||||
SetHyperDashState();
|
||||
|
||||
if (result.IsHit)
|
||||
CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle;
|
||||
@ -292,6 +300,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
if (wasHyperDashing)
|
||||
runHyperDashStateTransition(false);
|
||||
|
||||
lastHyperDashStartTime = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -301,6 +311,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
if (!wasHyperDashing)
|
||||
runHyperDashStateTransition(true);
|
||||
|
||||
lastHyperDashStartTime = Time.Current;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
[TestCase("zero-length-slider")]
|
||||
[TestCase("20544")]
|
||||
[TestCase("100374")]
|
||||
[TestCase("1450162")]
|
||||
public void Test(string name) => base.Test(name);
|
||||
|
||||
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
||||
|
@ -0,0 +1,42 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneManiaModAutoplay : ModTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestPerfectScoreOnShortHoldNote()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Autoplay = true,
|
||||
Beatmap = new ManiaBeatmap(new StageDefinition(1))
|
||||
{
|
||||
HitObjects = new List<ManiaHitObject>
|
||||
{
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = 100,
|
||||
EndTime = 100,
|
||||
},
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = 100.1,
|
||||
EndTime = 150,
|
||||
},
|
||||
}
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 4
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,297 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
StackLeniency: 0.7
|
||||
Mode: 0
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:5
|
||||
CircleSize:4
|
||||
OverallDifficulty:7
|
||||
ApproachRate:7.5
|
||||
SliderMultiplier:1.4
|
||||
SliderTickRate:1
|
||||
|
||||
[Events]
|
||||
//Background and Video events
|
||||
//Break Periods
|
||||
//Storyboard Layer 0 (Background)
|
||||
//Storyboard Layer 1 (Fail)
|
||||
//Storyboard Layer 2 (Pass)
|
||||
//Storyboard Layer 3 (Foreground)
|
||||
//Storyboard Sound Samples
|
||||
|
||||
[TimingPoints]
|
||||
1107,365.853658536585,4,2,1,50,1,0
|
||||
1107,-166.666666666667,4,2,1,50,0,0
|
||||
6960,-111.111111111111,4,2,1,50,0,0
|
||||
8424,-100,4,2,1,50,0,0
|
||||
48119,-125,4,2,1,50,0,0
|
||||
52143,-100,4,2,1,50,0,0
|
||||
62570,-100,4,2,1,60,0,1
|
||||
85985,-100,4,2,1,50,0,0
|
||||
97692,-100,4,2,1,30,0,0
|
||||
99155,-100,4,2,1,20,0,0
|
||||
100619,-100,4,2,1,5,0,0
|
||||
|
||||
[HitObjects]
|
||||
38,247,1107,6,0,P|96:269|170:192,1,167.999994873047,2|0,0:0|0:0,0:0:0:0:
|
||||
201,128,2570,6,0,L|205:221,1,83.9999974365235,2|0,0:0|0:0,0:0:0:0:
|
||||
242,230,3302,2,0,L|234:324,1,83.9999974365235,2|0,0:0|0:0,0:0:0:0:
|
||||
205,343,4033,6,0,P|246:296|351:314,1,167.999994873047,2|0,0:0|0:0,0:0:0:0:
|
||||
400,368,5497,6,0,L|412:269,1,83.9999974365235,6|0,0:0|0:0,0:0:0:0:
|
||||
436,251,6228,2,0,P|425:203|408:153,1,83.9999974365235,2|0,0:0|0:0,0:0:0:0:
|
||||
304,200,6960,6,0,P|262:186|234:181,1,62.9999980773926,6|0,0:0|0:0,0:0:0:0:
|
||||
202,179,7326,1,8,0:0:0:0:
|
||||
276,94,7509,2,0,P|313:92|353:87,1,62.9999980773926,2|0,0:0|0:0,0:0:0:0:
|
||||
398,31,7875,1,2,0:0:0:0:
|
||||
464,81,8058,2,0,L|450:150,1,62.9999980773926,2|0,0:0|0:0,0:0:0:0:
|
||||
449,230,8424,6,0,P|347:206|306:217,1,140,2|8,0:0|0:0,0:0:0:0:
|
||||
229,273,8972,2,0,P|225:339|235:361,1,70,2|0,0:0|0:0,0:0:0:0:
|
||||
304,313,9338,1,8,0:0:0:0:
|
||||
224,190,9521,1,2,0:0:0:0:
|
||||
296,45,9887,6,0,P|297:97|288:125,1,70,6|0,0:0|0:0,0:0:0:0:
|
||||
224,190,10253,1,8,0:0:0:0:
|
||||
167,118,10436,1,8,0:0:0:0:
|
||||
76,126,10619,1,8,0:0:0:0:
|
||||
39,209,10802,1,8,0:0:0:0:
|
||||
93,282,10985,1,10,0:0:0:0:
|
||||
184,280,11167,1,10,0:0:0:0:
|
||||
102,136,12814,5,2,0:0:0:0:
|
||||
102,136,13180,2,0,L|199:130,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
256,167,13546,2,0,L|339:161,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
408,201,13911,2,0,P|454:176|471:143,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
373,54,14277,6,0,L|396:137,2,70,6|0|8,0:0|0:0|0:0,0:0:0:0:
|
||||
305,111,14826,2,0,L|287:274,1,140,0|2,0:0|0:0,0:0:0:0:
|
||||
262,337,15375,2,0,L|349:327,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
419,354,15741,1,8,0:0:0:0:
|
||||
477,197,16106,6,0,P|423:197|385:209,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
321,170,16472,2,0,P|278:190|253:219,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
171,213,16838,2,0,P|152:259|158:304,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
305,294,17204,6,0,L|224:278,2,70,6|0|8,0:0|0:0|0:0,0:0:0:0:
|
||||
310,202,17753,2,0,L|149:214,1,140,0|2,0:0|0:0,0:0:0:0:
|
||||
84,244,18302,2,0,L|92:152,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
47,93,18667,6,0,P|78:53|176:80,1,140,6|8,0:0|0:0,0:0:0:0:
|
||||
218,130,19216,1,0,0:0:0:0:
|
||||
299,88,19399,2,0,L|387:91,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
458,106,19765,2,0,P|447:139|444:205,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
455,274,20131,5,2,0:0:0:0:
|
||||
366,292,20314,2,0,L|353:211,1,70,0|8,0:0|0:0,0:0:0:0:
|
||||
277,173,20680,2,0,L|253:342,1,140,0|2,0:0|0:0,0:0:0:0:
|
||||
322,376,21228,2,0,P|368:368|416:370,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
500,287,21594,6,0,P|427:273|362:293,2,140,6|8|8,0:0|0:0|0:0,0:0:0:0:
|
||||
496,111,22509,1,8,0:0:0:0:
|
||||
499,189,22692,2,0,L|418:191,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
344,164,23058,5,6,0:0:0:0:
|
||||
344,164,23241,1,12,0:0:0:0:
|
||||
261,326,23606,2,0,L|246:178,1,140,8|2,0:0|0:0,0:0:0:0:
|
||||
277,100,24155,2,0,P|225:99|196:109,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
165,273,24521,5,6,0:0:0:0:
|
||||
83,235,24704,2,0,L|93:81,1,140,0|0,0:0|0:0,0:0:0:0:
|
||||
21,37,25253,2,0,L|1:120,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0:
|
||||
110,17,25802,1,0,0:0:0:0:
|
||||
172,83,25985,5,2,0:0:0:0:
|
||||
236,19,26167,2,0,P|223:70|227:170,1,140,0|0,0:0|0:0,0:0:0:0:
|
||||
293,216,26716,2,0,P|316:165|314:134,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0:
|
||||
206,245,27265,1,0,0:0:0:0:
|
||||
274,305,27448,5,2,0:0:0:0:
|
||||
194,348,27631,2,0,L|363:332,1,140,0|0,0:0|0:0,0:0:0:0:
|
||||
424,336,28180,1,2,0:0:0:0:
|
||||
431,245,28363,2,0,P|381:252|354:276,2,70,0|8|0,0:0|0:0|0:0,0:0:0:0:
|
||||
509,291,28911,6,0,L|496:128,1,140,2|8,0:0|0:0,0:0:0:0:
|
||||
504,60,29460,1,0,0:0:0:0:
|
||||
417,34,29643,2,0,L|402:183,1,140,2|8,0:0|0:0,0:0:0:0:
|
||||
365,262,30192,1,0,0:0:0:0:
|
||||
295,202,30375,5,2,0:0:0:0:
|
||||
309,112,30558,2,0,P|282:172|196:176,1,140,0|0,0:0|0:0,0:0:0:0:
|
||||
148,120,31106,2,0,P|189:99|225:99,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0:
|
||||
129,209,31655,1,0,0:0:0:0:
|
||||
63,146,31838,5,2,0:0:0:0:
|
||||
16,67,32021,2,0,L|27:220,1,140,0|0,0:0|0:0,0:0:0:0:
|
||||
23,297,32570,2,0,P|81:286|111:290,1,70,2|0,0:0|0:0,0:0:0:0:
|
||||
173,327,32936,1,8,0:0:0:0:
|
||||
338,251,33302,6,0,P|268:254|227:199,1,140,2|8,0:0|0:0,0:0:0:0:
|
||||
203,114,33850,2,0,L|185:262,1,140,0|0,0:0|0:0,0:0:0:0:
|
||||
244,323,34399,1,8,0:0:0:0:
|
||||
334,335,34582,1,0,0:0:0:0:
|
||||
419,219,34765,6,0,L|410:304,1,70,2|0,0:0|0:0,0:0:0:0:
|
||||
338,251,35131,1,8,0:0:0:0:
|
||||
301,111,35314,2,0,L|301:190,1,70,6|0,0:0|0:0,0:0:0:0:
|
||||
383,141,35680,1,8,0:0:0:0:
|
||||
462,97,35863,2,0,P|427:64|393:54,1,70,2|0,0:0|0:0,0:0:0:0:
|
||||
321,23,36228,5,2,0:0:0:0:
|
||||
237,60,36411,1,0,0:0:0:0:
|
||||
148,38,36594,2,0,P|107:33|56:43,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
86,125,36960,2,0,P|51:125|17:117,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0:
|
||||
175,123,37509,1,0,0:0:0:0:
|
||||
129,201,37692,5,2,0:0:0:0:
|
||||
198,259,37875,1,0,0:0:0:0:
|
||||
205,349,38058,2,0,P|251:330|284:326,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
352,285,38424,2,0,P|361:318|357:353,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0:
|
||||
282,239,38972,1,0,0:0:0:0:
|
||||
362,195,39155,5,2,0:0:0:0:
|
||||
436,142,39338,2,0,P|398:115|354:112,1,70,0|8,0:0|0:0,0:0:0:0:
|
||||
286,92,39704,2,0,L|451:74,1,140,0|0,0:0|0:0,0:0:0:0:
|
||||
512,118,40253,2,0,L|494:198,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
430,297,40619,6,0,P|423:236|336:195,1,140,2|8,0:0|0:0,0:0:0:0:
|
||||
282,239,41167,1,0,0:0:0:0:
|
||||
209,184,41350,2,0,L|222:112,1,70,2|2,0:0|0:0,0:0:0:0:
|
||||
177,34,41716,2,0,P|230:26|269:38,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
307,95,42082,5,2,0:0:0:0:
|
||||
363,23,42265,2,0,L|359:114,1,70,0|8,0:0|0:0,0:0:0:0:
|
||||
360,184,42631,1,0,0:0:0:0:
|
||||
450,191,42814,2,0,P|443:145|424:119,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0:
|
||||
393,263,43363,1,0,0:0:0:0:
|
||||
304,242,43546,5,2,0:0:0:0:
|
||||
241,308,43728,1,0,0:0:0:0:
|
||||
167,256,43911,2,0,P|205:228|245:226,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
166,341,44277,2,0,P|118:325|90:289,1,70,2|0,0:0|0:0,0:0:0:0:
|
||||
125,177,44643,2,0,P|168:152|201:153,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
276,132,45009,6,0,L|119:105,1,140,2|8,0:0|0:0,0:0:0:0:
|
||||
52,74,45558,2,0,L|210:57,1,140,2|0,0:0|0:0,0:0:0:0:
|
||||
277,28,46106,1,8,0:0:0:0:
|
||||
349,82,46289,1,0,0:0:0:0:
|
||||
425,32,46472,6,0,L|451:110,2,70,6|2|8,0:0|0:0|0:0,0:0:0:0:
|
||||
349,82,47021,2,0,L|344:235,1,140,2|8,0:0|0:0,0:0:0:0:
|
||||
372,308,47570,1,2,0:0:0:0:
|
||||
170,324,47936,5,2,0:0:0:0:
|
||||
99,286,48119,2,0,L|112:112,1,168,2|2,0:0|0:0,0:0:0:0:
|
||||
64,48,48850,2,0,P|125:36|195:111,1,168,2|2,0:0|0:0,0:0:0:0:
|
||||
199,189,49582,6,0,L|369:166,1,168,2|2,0:0|0:0,0:0:0:0:
|
||||
413,97,50314,2,0,P|390:180|377:274,1,168,2|2,0:0|0:0,0:0:0:0:
|
||||
347,339,51046,6,0,P|424:333|463:251,1,168,2|2,0:0|0:0,0:0:0:0:
|
||||
473,175,51777,2,0,L|477:105,1,56,2|2,0:0|0:0,0:0:0:0:
|
||||
446,24,52143,6,0,P|363:22|308:82,1,140,12|2,0:0|0:0,0:0:0:0:
|
||||
282,138,52692,1,8,0:0:0:0:
|
||||
193,118,52875,2,0,L|213:281,1,140,2|8,0:0|0:0,0:0:0:0:
|
||||
225,347,53424,2,0,P|268:328|286:301,1,70,2|0,0:0|0:0,0:0:0:0:
|
||||
304,222,53789,5,2,0:0:0:0:
|
||||
385,263,53972,1,0,0:0:0:0:
|
||||
462,214,54155,2,0,P|421:185|383:179,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
322,136,54521,2,0,P|360:105|400:93,1,70,2|0,0:0|0:0,0:0:0:0:
|
||||
469,107,54887,2,0,L|483:24,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
390,22,55253,6,0,L|223:30,1,140,2|8,0:0|0:0,0:0:0:0:
|
||||
180,87,55802,1,0,0:0:0:0:
|
||||
230,162,55985,2,0,L|391:154,1,140,2|8,0:0|0:0,0:0:0:0:
|
||||
430,223,56533,1,0,0:0:0:0:
|
||||
407,311,56716,6,0,P|356:347|285:307,1,140,2|8,0:0|0:0,0:0:0:0:
|
||||
236,245,57265,1,0,0:0:0:0:
|
||||
145,237,57448,2,0,L|162:316,1,70,2|0,0:0|0:0,0:0:0:0:
|
||||
233,360,57814,6,0,P|185:349|142:350,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
11,311,58180,2,0,P|64:302|104:306,1,70,2|0,0:0|0:0,0:0:0:0:
|
||||
213,248,58546,2,0,P|162:237|130:237,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
1,194,58911,2,0,P|47:183|74:185,1,70,2|0,0:0|0:0,0:0:0:0:
|
||||
234,142,59277,2,0,P|175:129|152:128,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
12,26,59643,6,0,P|66:38|71:140,1,140,2|8,0:0|0:0,0:0:0:0:
|
||||
1,194,60192,1,0,0:0:0:0:
|
||||
84,230,60375,1,2,0:0:0:0:
|
||||
173,216,60558,1,8,0:0:0:0:
|
||||
173,216,60649,1,8,0:0:0:0:
|
||||
173,216,60741,1,8,0:0:0:0:
|
||||
263,213,60924,1,2,0:0:0:0:
|
||||
345,174,61106,6,0,P|320:144|286:130,1,70,2|0,0:0|0:0,0:0:0:0:
|
||||
200,134,61472,1,8,0:0:0:0:
|
||||
249,57,61655,2,0,L|263:12,2,35,12|8|8,0:0|0:0|0:0,0:0:0:0:
|
||||
157,64,62021,2,0,L|153:13,2,35,12|8|8,0:0|0:0|0:0,0:0:0:0:
|
||||
118,150,62387,1,2,0:0:0:0:
|
||||
101,260,62570,6,0,P|207:236|257:243,1,140,2|8,0:0|0:0,0:0:0:0:
|
||||
328,304,63119,1,0,0:0:0:0:
|
||||
434,156,63302,2,0,P|373:157|329:217,1,140,2|8,0:0|0:0,0:0:0:0:
|
||||
408,230,63850,1,2,0:0:0:0:
|
||||
483,215,64033,5,6,0:0:0:0:
|
||||
508,142,64216,1,0,0:0:0:0:
|
||||
482,69,64399,1,8,0:0:0:0:
|
||||
413,34,64582,2,0,P|336:30|256:49,1,140,0|2,0:0|0:0,0:0:0:0:
|
||||
150,97,65131,2,0,P|190:97|243:107,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
257,168,65497,6,0,L|225:323,1,140,2|8,0:0|0:0,0:0:0:0:
|
||||
155,329,66046,1,0,0:0:0:0:
|
||||
20,204,66228,2,0,P|92:202|133:271,1,140,8|8,0:0|0:0,0:0:0:0:
|
||||
56,274,66777,1,2,0:0:0:0:
|
||||
18,125,66960,6,0,L|93:119,1,70,6|0,0:0|0:0,0:0:0:0:
|
||||
162,156,67326,1,8,0:0:0:0:
|
||||
223,52,67509,2,0,L|227:219,1,140,0|2,0:0|0:0,0:0:0:0:
|
||||
266,263,68058,2,0,P|300:229|308:199,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
298,95,68424,6,0,L|458:75,1,140,6|8,0:0|0:0,0:0:0:0:
|
||||
512,164,68972,2,0,L|358:154,1,140,0|2,0:0|0:0,0:0:0:0:
|
||||
306,209,69521,1,8,0:0:0:0:
|
||||
342,334,69704,6,0,P|361:289|369:244,1,70,2|6,0:0|0:0,0:0:0:0:
|
||||
250,277,70070,2,0,P|223:228|219:186,1,70,0|8,0:0|0:0,0:0:0:0:
|
||||
272,128,70436,1,0,0:0:0:0:
|
||||
172,111,70619,2,0,L|343:97,1,140,8|8,0:0|0:0,0:0:0:0:
|
||||
385,128,71167,1,2,0:0:0:0:
|
||||
494,63,71350,6,0,L|413:54,1,70,6|0,0:0|0:0,0:0:0:0:
|
||||
385,128,71716,2,0,L|475:140,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
467,217,72082,2,0,L|386:208,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
358,282,72448,2,0,L|448:294,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
498,339,72814,5,12,0:0:0:0:
|
||||
498,339,72997,1,12,0:0:0:0:
|
||||
301,343,73363,1,8,0:0:0:0:
|
||||
211,173,73728,2,0,L|221:216,2,35,2|2|8,0:0|0:0|0:0,0:0:0:0:
|
||||
250,100,74094,1,2,0:0:0:0:
|
||||
123,92,74277,6,0,P|129:156|129:236,1,140,2|8,0:0|0:0,0:0:0:0:
|
||||
109,321,74826,1,0,0:0:0:0:
|
||||
211,173,75009,2,0,P|266:165|333:237,1,140,8|8,0:0|0:0,0:0:0:0:
|
||||
341,302,75558,1,2,0:0:0:0:
|
||||
418,272,75741,5,6,0:0:0:0:
|
||||
484,322,75924,1,0,0:0:0:0:
|
||||
407,352,76106,1,8,0:0:0:0:
|
||||
341,302,76289,2,0,L|364:147,1,140,0|2,0:0|0:0,0:0:0:0:
|
||||
269,60,76838,2,0,P|315:69|349:94,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
269,150,77204,6,0,P|228:160|114:139,1,140,2|8,0:0|0:0,0:0:0:0:
|
||||
49,80,77753,1,0,0:0:0:0:
|
||||
39,235,77936,2,0,P|103:222|160:277,1,140,8|8,0:0|0:0,0:0:0:0:
|
||||
82,297,78485,1,2,0:0:0:0:
|
||||
227,326,78667,6,0,L|233:241,1,70,4|0,0:0|0:0,0:0:0:0:
|
||||
269,150,79033,1,8,0:0:0:0:
|
||||
408,194,79216,2,0,P|359:172|271:187,1,140,0|2,0:0|0:0,0:0:0:0:
|
||||
409,281,79765,2,0,P|447:272|478:250,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
497,168,80131,6,0,L|481:332,1,140,6|8,0:0|0:0,0:0:0:0:
|
||||
389,365,80680,2,0,L|376:198,1,140,0|2,0:0|0:0,0:0:0:0:
|
||||
414,157,81228,1,8,0:0:0:0:
|
||||
229,89,81411,6,0,P|304:91|338:167,1,140,2|0,0:0|0:0,0:0:0:0:
|
||||
290,222,81960,1,8,0:0:0:0:
|
||||
211,214,82143,1,8,0:0:0:0:
|
||||
93,155,82326,2,0,P|137:143|172:150,1,70,2|2,0:0|0:0,0:0:0:0:
|
||||
235,301,82692,2,0,P|177:296|141:279,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
68,244,83058,6,0,L|72:328,1,70,6|0,0:0|0:0,0:0:0:0:
|
||||
166,292,83424,2,0,L|157:372,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
254,227,83789,2,0,L|258:310,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
345,265,84155,2,0,L|336:349,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
331,175,84521,5,2,0:0:0:0:
|
||||
416,205,84704,1,2,0:0:0:0:
|
||||
481,141,84887,1,8,0:0:0:0:
|
||||
431,64,85070,2,0,L|444:26,2,35,8|8|2,0:0|0:0|0:0,0:0:0:0:
|
||||
339,79,85436,2,0,L|341:39,2,35,8|8|8,0:0|0:0|0:0,0:0:0:0:
|
||||
256,109,85802,1,2,0:0:0:0:
|
||||
165,97,85985,6,0,P|167:150|164:187,1,70,2|0,0:0|0:0,0:0:0:0:
|
||||
117,244,86350,2,0,P|163:241|204:235,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
229,317,86716,2,0,P|273:305|300:294,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
365,354,87082,2,0,P|404:334|430:310,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
352,230,87448,6,0,L|271:216,2,70,6|0|8,0:0|0:0|0:0,0:0:0:0:
|
||||
378,142,87997,2,0,L|222:144,1,140,0|2,0:0|0:0,0:0:0:0:
|
||||
152,112,88546,2,0,L|166:214,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
139,270,88911,5,8,0:0:0:0:
|
||||
12,138,89277,2,0,L|29:55,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
91,5,89643,2,0,L|104:97,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
153,149,90009,2,0,L|175:78,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
279,36,90375,6,0,L|357:27,2,70,6|0|8,0:0|0:0|0:0,0:0:0:0:
|
||||
248,122,90924,2,0,L|398:125,1,140,0|2,0:0|0:0,0:0:0:0:
|
||||
479,123,91472,2,0,P|468:170|445:195,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
365,204,91838,6,0,P|414:220|409:320,1,140,6|8,0:0|0:0,0:0:0:0:
|
||||
354,354,92387,1,0,0:0:0:0:
|
||||
262,353,92570,2,0,L|271:273,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
297,196,92936,2,0,P|243:198|216:215,1,70,8|0,0:0|0:0,0:0:0:0:
|
||||
172,276,93302,5,6,0:0:0:0:
|
||||
137,360,93485,2,0,L|127:265,1,70,0|8,0:0|0:0,0:0:0:0:
|
||||
81,212,93850,2,0,P|93:138|118:67,1,140,0|2,0:0|0:0,0:0:0:0:
|
||||
170,4,94399,2,0,P|195:37|204:74,1,70,8|2,0:0|0:0,0:0:0:0:
|
||||
186,153,94765,6,0,L|340:139,1,140,6|8,0:0|0:0,0:0:0:0:
|
||||
408,101,95314,1,2,0:0:0:0:
|
||||
443,184,95497,1,6,0:0:0:0:
|
||||
369,237,95680,2,0,L|300:224,2,70,8|8|2,0:0|0:0|0:0,0:0:0:0:
|
||||
448,282,96228,5,12,0:0:0:0:
|
||||
448,282,96411,1,12,0:0:0:0:
|
||||
270,320,96777,1,8,0:0:0:0:
|
||||
313,143,97143,1,8,0:0:0:0:
|
||||
377,314,97509,1,8,0:0:0:0:
|
||||
256,192,97692,12,0,100619,0:0:0:0:
|
@ -57,14 +57,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
|
||||
public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty)
|
||||
{
|
||||
if (new ManiaRuleset().RulesetInfo.Equals(difficulty.SourceRuleset))
|
||||
return GetColumnCountForNonConvert(difficulty);
|
||||
|
||||
double roundedCircleSize = Math.Round(difficulty.CircleSize);
|
||||
|
||||
if (new ManiaRuleset().RulesetInfo.Equals(difficulty.SourceRuleset))
|
||||
return (int)Math.Max(1, roundedCircleSize);
|
||||
|
||||
double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty);
|
||||
|
||||
int countSliderOrSpinner = difficulty.TotalObjectCount - difficulty.CircleCount;
|
||||
float percentSpecialObjects = (float)countSliderOrSpinner / difficulty.TotalObjectCount;
|
||||
int countSliderOrSpinner = difficulty.EndTimeObjectCount;
|
||||
|
||||
// In osu!stable, this division appears as if it happens on floats, but due to release-mode
|
||||
// optimisations, it actually ends up happening on doubles.
|
||||
double percentSpecialObjects = (double)countSliderOrSpinner / difficulty.TotalObjectCount;
|
||||
|
||||
if (percentSpecialObjects < 0.2)
|
||||
return 7;
|
||||
@ -76,12 +80,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
return Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
|
||||
}
|
||||
|
||||
public static int GetColumnCountForNonConvert(IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
double roundedCircleSize = Math.Round(difficulty.CircleSize);
|
||||
return (int)Math.Max(1, roundedCircleSize);
|
||||
}
|
||||
|
||||
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
|
||||
|
||||
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
|
||||
|
@ -4,6 +4,7 @@
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Filter;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
public bool Matches(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
return !keys.HasFilter || (beatmapInfo.Ruleset.OnlineID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo.Difficulty)));
|
||||
return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo)));
|
||||
}
|
||||
|
||||
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
|
||||
|
@ -420,6 +420,9 @@ namespace osu.Game.Rulesets.Mania
|
||||
public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection();
|
||||
|
||||
public override DifficultySection CreateEditorDifficultySection() => new ManiaDifficultySection();
|
||||
|
||||
public int GetKeyCount(IBeatmapInfo beatmapInfo)
|
||||
=> ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo));
|
||||
}
|
||||
|
||||
public enum PlayfieldType
|
||||
|
@ -87,15 +87,22 @@ namespace osu.Game.Rulesets.Mania.Replays
|
||||
private double calculateReleaseTime(HitObject currentObject, HitObject? nextObject)
|
||||
{
|
||||
double endTime = currentObject.GetEndTime();
|
||||
double releaseDelay = RELEASE_DELAY;
|
||||
|
||||
if (currentObject is HoldNote)
|
||||
// hold note releases must be timed exactly.
|
||||
return endTime;
|
||||
if (currentObject is HoldNote hold)
|
||||
{
|
||||
if (hold.Duration > 0)
|
||||
// hold note releases must be timed exactly.
|
||||
return endTime;
|
||||
|
||||
// Special case for super short hold notes
|
||||
releaseDelay = 1;
|
||||
}
|
||||
|
||||
bool canDelayKeyUpFully = nextObject == null ||
|
||||
nextObject.StartTime > endTime + RELEASE_DELAY;
|
||||
nextObject.StartTime > endTime + releaseDelay;
|
||||
|
||||
return endTime + (canDelayKeyUpFully ? RELEASE_DELAY : (nextObject.AsNonNull().StartTime - endTime) * 0.9);
|
||||
return endTime + (canDelayKeyUpFully ? releaseDelay : (nextObject.AsNonNull().StartTime - endTime) * 0.9);
|
||||
}
|
||||
|
||||
protected override HitObject? GetNextObject(int currentIndex)
|
||||
|
@ -234,7 +234,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
break;
|
||||
|
||||
default:
|
||||
// this is where things get fucked up.
|
||||
// this is where things get a bit messed up.
|
||||
// honestly there's three modes to handle here but they seem really pointless?
|
||||
// let's wait to see if anyone actually uses them in skins.
|
||||
if (bodySprite != null)
|
||||
|
@ -310,9 +310,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
assertPlaced(true);
|
||||
assertLength(760, tolerance: 10);
|
||||
assertLength(808, tolerance: 10);
|
||||
assertControlPointCount(5);
|
||||
assertControlPointType(0, PathType.BSpline(3));
|
||||
assertControlPointType(0, PathType.BSpline(4));
|
||||
assertControlPointType(1, null);
|
||||
assertControlPointType(2, null);
|
||||
assertControlPointType(3, null);
|
||||
@ -337,9 +337,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertPlaced(true);
|
||||
assertLength(600, tolerance: 10);
|
||||
assertControlPointCount(4);
|
||||
assertControlPointType(0, PathType.LINEAR);
|
||||
assertControlPointType(1, null);
|
||||
assertControlPointType(2, null);
|
||||
assertControlPointType(0, PathType.BSpline(4));
|
||||
assertControlPointType(1, PathType.BSpline(4));
|
||||
assertControlPointType(2, PathType.BSpline(4));
|
||||
assertControlPointType(3, null);
|
||||
}
|
||||
|
||||
|
@ -74,6 +74,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
simulateHit(obj, ref attributes);
|
||||
|
||||
attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore;
|
||||
attributes.BonusScore = legacyBonusScore;
|
||||
attributes.MaxCombo = combo;
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
@ -373,7 +373,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.LINEAR));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.PERFECT_CURVE));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.BEZIER));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(3)));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(4)));
|
||||
|
||||
if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull))
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL));
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
@ -41,15 +42,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
private int currentSegmentLength;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
private IPositionSnapProvider positionSnapProvider { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
private IDistanceSnapProvider distanceSnapProvider { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
private FreehandSliderToolboxGroup freehandToolboxGroup { get; set; }
|
||||
|
||||
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder();
|
||||
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
|
||||
|
||||
protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
|
||||
|
||||
@ -94,6 +98,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
bSplineBuilder.CornerThreshold = e.NewValue;
|
||||
Scheduler.AddOnce(updateSliderPathFromBSplineBuilder);
|
||||
}, true);
|
||||
|
||||
freehandToolboxGroup.CircleThreshold.BindValueChanged(e =>
|
||||
{
|
||||
Scheduler.AddOnce(updateSliderPathFromBSplineBuilder);
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,7 +206,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
base.OnDragEnd(e);
|
||||
|
||||
if (state == SliderPlacementState.Drawing)
|
||||
{
|
||||
bSplineBuilder.Finish();
|
||||
updateSliderPathFromBSplineBuilder();
|
||||
|
||||
// Change the state so it will snap the expected distance in endCurve.
|
||||
state = SliderPlacementState.Finishing;
|
||||
endCurve();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
@ -232,7 +248,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
if (state == SliderPlacementState.Drawing)
|
||||
{
|
||||
segmentStart.Type = PathType.BSpline(3);
|
||||
segmentStart.Type = PathType.BSpline(4);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -300,7 +316,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private void updateSlider()
|
||||
{
|
||||
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
|
||||
if (state == SliderPlacementState.Drawing)
|
||||
HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance;
|
||||
else
|
||||
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
|
||||
|
||||
bodyPiece.UpdateFrom(HitObject);
|
||||
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
|
||||
@ -309,53 +328,126 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private void updateSliderPathFromBSplineBuilder()
|
||||
{
|
||||
IReadOnlyList<Vector2> builderPoints = bSplineBuilder.ControlPoints;
|
||||
IReadOnlyList<List<Vector2>> builderPoints = bSplineBuilder.ControlPoints;
|
||||
|
||||
if (builderPoints.Count == 0)
|
||||
if (builderPoints.Count == 0 || builderPoints[0].Count == 0)
|
||||
return;
|
||||
|
||||
int lastSegmentStart = 0;
|
||||
PathType? lastPathType = null;
|
||||
|
||||
HitObject.Path.ControlPoints.Clear();
|
||||
|
||||
// Iterate through generated points, finding each segment and adding non-inheriting path types where appropriate.
|
||||
// Importantly, the B-Spline builder returns three Vector2s at the same location when a new segment is to be started.
|
||||
// Iterate through generated segments and adding non-inheriting path types where appropriate.
|
||||
for (int i = 0; i < builderPoints.Count; i++)
|
||||
{
|
||||
bool isLastPoint = i == builderPoints.Count - 1;
|
||||
bool isNewSegment = i < builderPoints.Count - 2 && builderPoints[i] == builderPoints[i + 1] && builderPoints[i] == builderPoints[i + 2];
|
||||
bool isLastSegment = i == builderPoints.Count - 1;
|
||||
var segment = builderPoints[i];
|
||||
|
||||
if (isNewSegment || isLastPoint)
|
||||
if (segment.Count == 0)
|
||||
continue;
|
||||
|
||||
// Replace this segment with a circular arc if it is a reasonable substitute.
|
||||
var circleArcSegment = tryCircleArc(segment);
|
||||
|
||||
if (circleArcSegment is not null)
|
||||
{
|
||||
int pointsInSegment = i - lastSegmentStart;
|
||||
|
||||
// Where possible, we can use the simpler LINEAR path type.
|
||||
PathType? pathType = pointsInSegment == 1 ? PathType.LINEAR : PathType.BSpline(3);
|
||||
|
||||
// Linear segments can be combined, as two adjacent linear sections are computationally the same as one with the points combined.
|
||||
if (lastPathType == pathType && lastPathType == PathType.LINEAR)
|
||||
pathType = null;
|
||||
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[lastSegmentStart], pathType));
|
||||
for (int j = lastSegmentStart + 1; j < i; j++)
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[j]));
|
||||
|
||||
if (isLastPoint)
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[i]));
|
||||
|
||||
// Skip the redundant duplicated points (see isNewSegment above) which have been coalesced into a path type.
|
||||
lastSegmentStart = (i += 2);
|
||||
if (pathType != null) lastPathType = pathType;
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[0], PathType.PERFECT_CURVE));
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[1]));
|
||||
}
|
||||
else
|
||||
{
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(segment[0], PathType.BSpline(4)));
|
||||
for (int j = 1; j < segment.Count - 1; j++)
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(segment[j]));
|
||||
}
|
||||
|
||||
if (isLastSegment)
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(segment[^1]));
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2[] tryCircleArc(List<Vector2> segment)
|
||||
{
|
||||
if (segment.Count < 3 || freehandToolboxGroup?.CircleThreshold.Value == 0) return null;
|
||||
|
||||
// Assume the segment creates a reasonable circular arc and then check if it reasonable
|
||||
var points = PathApproximator.BSplineToPiecewiseLinear(segment.ToArray(), bSplineBuilder.Degree);
|
||||
var circleArcControlPoints = new[] { points[0], points[points.Count / 2], points[^1] };
|
||||
var circleArc = new CircularArcProperties(circleArcControlPoints);
|
||||
|
||||
if (!circleArc.IsValid) return null;
|
||||
|
||||
double length = circleArc.ThetaRange * circleArc.Radius;
|
||||
|
||||
if (length > 1000) return null;
|
||||
|
||||
double loss = 0;
|
||||
Vector2? lastPoint = null;
|
||||
Vector2? lastVec = null;
|
||||
Vector2? lastVec2 = null;
|
||||
int? lastDir = null;
|
||||
int? lastDir2 = null;
|
||||
double totalWinding = 0;
|
||||
|
||||
// Loop through the points and check if they are not too far away from the circular arc.
|
||||
// Also make sure it curves monotonically in one direction and at most one loop is done.
|
||||
foreach (var point in points)
|
||||
{
|
||||
var vec = point - circleArc.Centre;
|
||||
loss += Math.Pow((vec.Length - circleArc.Radius) / length, 2);
|
||||
|
||||
if (lastVec.HasValue)
|
||||
{
|
||||
double det = lastVec.Value.X * vec.Y - lastVec.Value.Y * vec.X;
|
||||
int dir = Math.Sign(det);
|
||||
|
||||
if (dir == 0)
|
||||
continue;
|
||||
|
||||
if (lastDir.HasValue && dir != lastDir)
|
||||
return null; // Circle center is not inside the polygon
|
||||
|
||||
lastDir = dir;
|
||||
}
|
||||
|
||||
lastVec = vec;
|
||||
|
||||
if (lastPoint.HasValue)
|
||||
{
|
||||
var vec2 = point - lastPoint.Value;
|
||||
|
||||
if (lastVec2.HasValue)
|
||||
{
|
||||
double dot = Vector2.Dot(vec2, lastVec2.Value);
|
||||
double det = lastVec2.Value.X * vec2.Y - lastVec2.Value.Y * vec2.X;
|
||||
double angle = Math.Atan2(det, dot);
|
||||
int dir2 = Math.Sign(angle);
|
||||
|
||||
if (dir2 == 0)
|
||||
continue;
|
||||
|
||||
if (lastDir2.HasValue && dir2 != lastDir2)
|
||||
return null; // Curvature changed, like in an S-shape
|
||||
|
||||
totalWinding += Math.Abs(angle);
|
||||
lastDir2 = dir2;
|
||||
}
|
||||
|
||||
lastVec2 = vec2;
|
||||
}
|
||||
|
||||
lastPoint = point;
|
||||
}
|
||||
|
||||
loss /= points.Count;
|
||||
|
||||
return loss > freehandToolboxGroup?.CircleThreshold.Value || totalWinding > MathHelper.TwoPi ? null : circleArcControlPoints;
|
||||
}
|
||||
|
||||
private enum SliderPlacementState
|
||||
{
|
||||
Initial,
|
||||
ControlPoints,
|
||||
Drawing
|
||||
Drawing,
|
||||
Finishing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,10 +17,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
}
|
||||
|
||||
public BindableFloat Tolerance { get; } = new BindableFloat(1.5f)
|
||||
public BindableFloat Tolerance { get; } = new BindableFloat(1.8f)
|
||||
{
|
||||
MinValue = 0.05f,
|
||||
MaxValue = 3f,
|
||||
MaxValue = 2.0f,
|
||||
Precision = 0.01f
|
||||
};
|
||||
|
||||
@ -31,8 +31,15 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
Precision = 0.01f
|
||||
};
|
||||
|
||||
public BindableFloat CircleThreshold { get; } = new BindableFloat(0.0015f)
|
||||
{
|
||||
MinValue = 0f,
|
||||
MaxValue = 0.005f,
|
||||
Precision = 0.0001f
|
||||
};
|
||||
|
||||
// We map internal ranges to a more standard range of values for display to the user.
|
||||
private readonly BindableInt displayTolerance = new BindableInt(40)
|
||||
private readonly BindableInt displayTolerance = new BindableInt(90)
|
||||
{
|
||||
MinValue = 5,
|
||||
MaxValue = 100
|
||||
@ -44,8 +51,15 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
MaxValue = 100
|
||||
};
|
||||
|
||||
private readonly BindableInt displayCircleThreshold = new BindableInt(30)
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 100
|
||||
};
|
||||
|
||||
private ExpandableSlider<int> toleranceSlider = null!;
|
||||
private ExpandableSlider<int> cornerThresholdSlider = null!;
|
||||
private ExpandableSlider<int> circleThresholdSlider = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
@ -59,6 +73,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
cornerThresholdSlider = new ExpandableSlider<int>
|
||||
{
|
||||
Current = displayCornerThreshold
|
||||
},
|
||||
circleThresholdSlider = new ExpandableSlider<int>
|
||||
{
|
||||
Current = displayCircleThreshold
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -83,18 +101,32 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
CornerThreshold.Value = displayToInternalCornerThreshold(threshold.NewValue);
|
||||
}, true);
|
||||
|
||||
displayCircleThreshold.BindValueChanged(threshold =>
|
||||
{
|
||||
circleThresholdSlider.ContractedLabelText = $"P. C. T.: {threshold.NewValue:N0}";
|
||||
circleThresholdSlider.ExpandedLabelText = $"Perfect Curve Threshold: {threshold.NewValue:N0}";
|
||||
|
||||
CircleThreshold.Value = displayToInternalCircleThreshold(threshold.NewValue);
|
||||
}, true);
|
||||
|
||||
Tolerance.BindValueChanged(tolerance =>
|
||||
displayTolerance.Value = internalToDisplayTolerance(tolerance.NewValue)
|
||||
);
|
||||
CornerThreshold.BindValueChanged(threshold =>
|
||||
displayCornerThreshold.Value = internalToDisplayCornerThreshold(threshold.NewValue)
|
||||
);
|
||||
CircleThreshold.BindValueChanged(threshold =>
|
||||
displayCircleThreshold.Value = internalToDisplayCircleThreshold(threshold.NewValue)
|
||||
);
|
||||
|
||||
float displayToInternalTolerance(float v) => v / 33f;
|
||||
int internalToDisplayTolerance(float v) => (int)Math.Round(v * 33f);
|
||||
float displayToInternalTolerance(float v) => v / 50f;
|
||||
int internalToDisplayTolerance(float v) => (int)Math.Round(v * 50f);
|
||||
|
||||
float displayToInternalCornerThreshold(float v) => v / 100f;
|
||||
int internalToDisplayCornerThreshold(float v) => (int)Math.Round(v * 100f);
|
||||
|
||||
float displayToInternalCircleThreshold(float v) => v / 20000f;
|
||||
int internalToDisplayCircleThreshold(float v) => (int)Math.Round(v * 20000f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@ -17,12 +16,5 @@ namespace osu.Game.Rulesets.Osu.Scoring
|
||||
|
||||
protected override HitEvent CreateHitEvent(JudgementResult result)
|
||||
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
|
||||
|
||||
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
|
||||
{
|
||||
return 700000 * comboProgress
|
||||
+ 300000 * Math.Pow(Accuracy.Value, 10) * accuracyProgress
|
||||
+ bonusPortion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
relativeGlowSize = Source.glowSize / Source.DrawSize.X;
|
||||
}
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
protected override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
drawGlow(renderer);
|
||||
|
@ -231,7 +231,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
points.AddRange(Source.SmokePoints.Skip(firstVisiblePointIndex).Take(futurePointIndex - firstVisiblePointIndex));
|
||||
}
|
||||
|
||||
public sealed override void Draw(IRenderer renderer)
|
||||
protected sealed override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
|
||||
|
@ -258,7 +258,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
private IUniformBuffer<CursorTrailParameters> cursorTrailParameters;
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
protected override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
|
||||
|
@ -75,6 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
simulateHit(obj, ref attributes);
|
||||
|
||||
attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore;
|
||||
attributes.BonusScore = legacyBonusScore;
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
105
osu.Game.Tests/Editing/Checks/CheckDelayedHitsoundsTest.cs
Normal file
105
osu.Game.Tests/Editing/Checks/CheckDelayedHitsoundsTest.cs
Normal file
@ -0,0 +1,105 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using ManagedBass;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Audio;
|
||||
|
||||
namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
[TestFixture]
|
||||
public class CheckDelayedHitsoundsTest
|
||||
{
|
||||
private CheckDelayedHitsounds check = null!;
|
||||
private IBeatmap beatmap = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
check = new CheckDelayedHitsounds();
|
||||
beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
BeatmapSet = new BeatmapSetInfo
|
||||
{
|
||||
Files =
|
||||
{
|
||||
new RealmNamedFileUsage(new RealmFile { Hash = "abcdef" }, "normal-hitnormal.wav"),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!Bass.Init(0) && Bass.LastError != Errors.Already)
|
||||
throw new AudioException("Could not initialize Bass.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoDelayedHitsounds()
|
||||
{
|
||||
using var resourceStream = TestResources.OpenResource("Samples/hitsound-no-delay.wav");
|
||||
Assert.IsEmpty(check.Run(getContext(resourceStream)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMinorDelayedHitsounds()
|
||||
{
|
||||
// 1 ms of silence -> 1 ms of noise at 0.3 amplitude -> hitsound transient
|
||||
// => The transient is delayed by 2 ms
|
||||
// Waveform: https://github.com/ppy/osu/assets/39100084/d5b9edbe-0ba2-401d-94b0-6d57228bdbd3
|
||||
using (var resourceStream = TestResources.OpenResource("Samples/hitsound-minor-delay.wav"))
|
||||
{
|
||||
var issues = check.Run(getContext(resourceStream)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckDelayedHitsounds.IssueTemplateMinorDelay);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDelayedHitsounds()
|
||||
{
|
||||
// 3 ms of silence -> 3 ms of noise at 0.3 amplitude -> hitsound transient
|
||||
// => The transient is delayed by 6 ms
|
||||
// Waveform: https://github.com/ppy/osu/assets/39100084/2509ff35-d908-414b-b7b9-583681348772
|
||||
using var resourceStream = TestResources.OpenResource("Samples/hitsound-delay.wav");
|
||||
|
||||
var issues = check.Run(getContext(resourceStream)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckDelayedHitsounds.IssueTemplateDelay);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestConsequentlyDelayedHitsounds()
|
||||
{
|
||||
// The hitsound is delayed by 10 ms
|
||||
// Waveform: https://github.com/ppy/osu/assets/39100084/3a7ede0d-8523-4b99-a222-3624cd208267
|
||||
using var resourceStream = TestResources.OpenResource("Samples/hitsound-consequent-delay.wav");
|
||||
|
||||
var issues = check.Run(getContext(resourceStream)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckDelayedHitsounds.IssueTemplateConsequentDelay);
|
||||
}
|
||||
|
||||
private BeatmapVerifierContext getContext(Stream? resourceStream)
|
||||
{
|
||||
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);
|
||||
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
|
||||
|
||||
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
|
||||
}
|
||||
}
|
||||
}
|
BIN
osu.Game.Tests/Resources/Samples/hitsound-consequent-delay.wav
Normal file
BIN
osu.Game.Tests/Resources/Samples/hitsound-consequent-delay.wav
Normal file
Binary file not shown.
BIN
osu.Game.Tests/Resources/Samples/hitsound-delay.wav
Normal file
BIN
osu.Game.Tests/Resources/Samples/hitsound-delay.wav
Normal file
Binary file not shown.
BIN
osu.Game.Tests/Resources/Samples/hitsound-minor-delay.wav
Normal file
BIN
osu.Game.Tests/Resources/Samples/hitsound-minor-delay.wav
Normal file
Binary file not shown.
BIN
osu.Game.Tests/Resources/Samples/hitsound-no-delay.wav
Normal file
BIN
osu.Game.Tests/Resources/Samples/hitsound-no-delay.wav
Normal file
Binary file not shown.
@ -45,11 +45,11 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
};
|
||||
}
|
||||
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Meh, 116_667)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Ok, 233_338)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Meh, 83_398)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Ok, 168_724)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Meh, 11_670)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Ok, 23_341)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Meh, 8_343)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Ok, 16_878)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Great, 100_033)]
|
||||
public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
|
||||
{
|
||||
@ -75,27 +75,27 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
/// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and 50% max combo.
|
||||
/// </remarks>
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 79_333)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 158_667)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 317_626)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 492_894)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 492_894)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 34_734)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 69_925)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 154_499)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 326_963)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 326_963)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 541_894)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 493_652)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 492_894)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 326_963)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 7_975)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 15_949)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 31_928)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 49_546)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 49_546)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 3_492)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 7_029)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 15_530)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 32_867)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 32_867)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 54_189)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 49_365)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 49_289)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 32_696)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 100_003)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 100_015)]
|
||||
public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore)
|
||||
@ -356,6 +356,27 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
Assert.That(actual, Is.EqualTo(expected).Within(Precision.FLOAT_EPSILON));
|
||||
}
|
||||
|
||||
[TestCase(HitResult.Great)]
|
||||
[TestCase(HitResult.LargeTickHit)]
|
||||
public void TestAccuracyUpdateFromIgnoreMiss(HitResult maxResult)
|
||||
{
|
||||
scoreProcessor.ApplyBeatmap(new Beatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new TestHitObject(maxResult, HitResult.IgnoreMiss)
|
||||
}
|
||||
});
|
||||
|
||||
var judgementResult = new JudgementResult(beatmap.HitObjects.Single(), new TestJudgement(maxResult, HitResult.IgnoreMiss))
|
||||
{
|
||||
Type = HitResult.IgnoreMiss
|
||||
};
|
||||
scoreProcessor.ApplyResult(judgementResult);
|
||||
|
||||
Assert.That(scoreProcessor.Accuracy.Value, Is.Not.EqualTo(1));
|
||||
}
|
||||
|
||||
private class TestJudgement : Judgement
|
||||
{
|
||||
public override HitResult MaxResult { get; }
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
public partial class TestSceneOpenEditorTimestamp : OsuGameTestScene
|
||||
{
|
||||
private Editor editor => (Editor)Game.ScreenStack.CurrentScreen;
|
||||
private Editor? editor => Game.ScreenStack.CurrentScreen as Editor;
|
||||
private EditorBeatmap editorBeatmap => editor.ChildrenOfType<EditorBeatmap>().Single();
|
||||
private EditorClock editorClock => editor.ChildrenOfType<EditorClock>().Single();
|
||||
|
||||
@ -111,18 +111,18 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
}
|
||||
|
||||
private void addStepScreenModeTo(EditorScreenMode screenMode) =>
|
||||
AddStep("change screen to " + screenMode, () => editor.Mode.Value = screenMode);
|
||||
AddStep("change screen to " + screenMode, () => editor!.Mode.Value = screenMode);
|
||||
|
||||
private void assertOnScreenAt(EditorScreenMode screen, double time)
|
||||
{
|
||||
AddAssert($"stayed on {screen} at {time}", () =>
|
||||
editor.Mode.Value == screen
|
||||
editor!.Mode.Value == screen
|
||||
&& editorClock.CurrentTime == time
|
||||
);
|
||||
}
|
||||
|
||||
private void assertMovedScreenTo(EditorScreenMode screen, string text = "moved to") =>
|
||||
AddAssert($"{text} {screen}", () => editor.Mode.Value == screen);
|
||||
AddAssert($"{text} {screen}", () => editor!.Mode.Value == screen);
|
||||
|
||||
private void setUpEditor(RulesetInfo ruleset)
|
||||
{
|
||||
@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
((PlaySongSelect)Game.ScreenStack.CurrentScreen)
|
||||
.Edit(beatmapSet.Beatmaps.Last(beatmap => beatmap.Ruleset.Name == ruleset.Name))
|
||||
);
|
||||
AddUntilStep("Wait for editor open", () => editor.ReadyForUse);
|
||||
AddUntilStep("Wait for editor open", () => editor?.ReadyForUse == true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -143,13 +143,13 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
|
||||
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
||||
|
||||
AddStep("set filter", () => songSelect.ChildrenOfType<SearchTextBox>().Single().Current.Value = "test");
|
||||
AddStep("set filter", () => filterControlTextBox().Current.Value = "test");
|
||||
AddStep("press back", () => InputManager.Click(MouseButton.Button1));
|
||||
|
||||
AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen == songSelect);
|
||||
AddAssert("filter cleared", () => string.IsNullOrEmpty(songSelect.ChildrenOfType<SearchTextBox>().Single().Current.Value));
|
||||
AddAssert("filter cleared", () => string.IsNullOrEmpty(filterControlTextBox().Current.Value));
|
||||
|
||||
AddStep("set filter again", () => songSelect.ChildrenOfType<SearchTextBox>().Single().Current.Value = "test");
|
||||
AddStep("set filter again", () => filterControlTextBox().Current.Value = "test");
|
||||
AddStep("open collections dropdown", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(songSelect.ChildrenOfType<CollectionDropdown>().Single());
|
||||
@ -163,10 +163,12 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
.ChildrenOfType<Dropdown<CollectionFilterMenuItem>.DropdownMenu>().Single().State == MenuState.Closed);
|
||||
|
||||
AddStep("press back a second time", () => InputManager.Click(MouseButton.Button1));
|
||||
AddAssert("filter cleared", () => string.IsNullOrEmpty(songSelect.ChildrenOfType<SearchTextBox>().Single().Current.Value));
|
||||
AddAssert("filter cleared", () => string.IsNullOrEmpty(filterControlTextBox().Current.Value));
|
||||
|
||||
AddStep("press back a third time", () => InputManager.Click(MouseButton.Button1));
|
||||
ConfirmAtMainMenu();
|
||||
|
||||
TextBox filterControlTextBox() => songSelect.ChildrenOfType<FilterControl.FilterControlTextBox>().Single();
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -631,7 +631,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddAssert("Nothing happened", () => this.ChildrenOfType<ReportChatPopover>().Any());
|
||||
AddStep("Set report data", () =>
|
||||
{
|
||||
var field = this.ChildrenOfType<ReportChatPopover>().Single().ChildrenOfType<OsuTextBox>().Single();
|
||||
var field = this.ChildrenOfType<ReportChatPopover>().Single().ChildrenOfType<OsuTextBox>().First();
|
||||
field.Current.Value = "test other";
|
||||
});
|
||||
|
||||
|
@ -262,7 +262,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddAssert("Nothing happened", () => this.ChildrenOfType<ReportCommentPopover>().Any());
|
||||
AddStep("Set report data", () =>
|
||||
{
|
||||
var field = this.ChildrenOfType<ReportCommentPopover>().Single().ChildrenOfType<OsuTextBox>().Single();
|
||||
var field = this.ChildrenOfType<ReportCommentPopover>().Single().ChildrenOfType<OsuTextBox>().First();
|
||||
field.Current.Value = report_text;
|
||||
var reason = this.ChildrenOfType<OsuEnumDropdown<CommentReportReason>>().Single();
|
||||
reason.Current.Value = CommentReportReason.Other;
|
||||
|
@ -49,12 +49,12 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
AddStep("reset mouse", () => InputManager.MoveMouseTo(settings));
|
||||
|
||||
if (beforeLoad)
|
||||
AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType<SearchTextBox>().First().Current.Value = "scaling");
|
||||
AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType<SettingsSearchTextBox>().First().Current.Value = "scaling");
|
||||
|
||||
AddUntilStep("wait for items to load", () => settings.SectionsContainer.ChildrenOfType<IFilterable>().Any());
|
||||
|
||||
if (!beforeLoad)
|
||||
AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType<SearchTextBox>().First().Current.Value = "scaling");
|
||||
AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType<SettingsSearchTextBox>().First().Current.Value = "scaling");
|
||||
|
||||
AddAssert("ensure all items match filter", () => settings.SectionsContainer
|
||||
.ChildrenOfType<SettingsSection>().Where(f => f.IsPresent)
|
||||
@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
|
||||
AddUntilStep("wait for items to load", () => settings.SectionsContainer.ChildrenOfType<IFilterable>().Any());
|
||||
|
||||
AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType<SearchTextBox>().First().Current.Value = "scaling");
|
||||
AddStep("set filter", () => settings.SectionsContainer.ChildrenOfType<SettingsSearchTextBox>().First().Current.Value = "scaling");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
AddStep("reset mouse", () => InputManager.MoveMouseTo(settings));
|
||||
|
||||
AddUntilStep("sections loaded", () => settings.SectionsContainer.Children.Count > 0);
|
||||
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
|
||||
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<SettingsSearchTextBox>().FirstOrDefault()?.HasFocus == true);
|
||||
|
||||
AddStep("open key binding subpanel", () =>
|
||||
{
|
||||
@ -106,13 +106,13 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
|
||||
AddUntilStep("binding panel textbox focused", () => settings
|
||||
.ChildrenOfType<KeyBindingPanel>().FirstOrDefault()?
|
||||
.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
|
||||
.ChildrenOfType<SettingsSearchTextBox>().FirstOrDefault()?.HasFocus == true);
|
||||
|
||||
AddStep("Press back", () => settings
|
||||
.ChildrenOfType<KeyBindingPanel>().FirstOrDefault()?
|
||||
.ChildrenOfType<SettingsSubPanel.BackButton>().FirstOrDefault()?.TriggerClick());
|
||||
|
||||
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
|
||||
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<SettingsSearchTextBox>().FirstOrDefault()?.HasFocus == true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
AddStep("reset mouse", () => InputManager.MoveMouseTo(settings));
|
||||
|
||||
AddUntilStep("sections loaded", () => settings.SectionsContainer.Children.Count > 0);
|
||||
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
|
||||
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<SettingsSearchTextBox>().FirstOrDefault()?.HasFocus == true);
|
||||
|
||||
AddStep("open key binding subpanel", () =>
|
||||
{
|
||||
@ -133,19 +133,19 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
|
||||
AddUntilStep("binding panel textbox focused", () => settings
|
||||
.ChildrenOfType<KeyBindingPanel>().FirstOrDefault()?
|
||||
.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
|
||||
.ChildrenOfType<SettingsSearchTextBox>().FirstOrDefault()?.HasFocus == true);
|
||||
|
||||
AddStep("Escape", () => InputManager.Key(Key.Escape));
|
||||
|
||||
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
|
||||
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<SettingsSearchTextBox>().FirstOrDefault()?.HasFocus == true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSearchTextBoxSelectedOnShow()
|
||||
{
|
||||
SearchTextBox searchTextBox = null!;
|
||||
SettingsSearchTextBox searchTextBox = null!;
|
||||
|
||||
AddStep("set text", () => (searchTextBox = settings.SectionsContainer.ChildrenOfType<SearchTextBox>().First()).Current.Value = "some text");
|
||||
AddStep("set text", () => (searchTextBox = settings.SectionsContainer.ChildrenOfType<SettingsSearchTextBox>().First()).Current.Value = "some text");
|
||||
AddAssert("no text selected", () => searchTextBox.SelectedText == string.Empty);
|
||||
AddRepeatStep("toggle visibility", () => settings.ToggleVisibility(), 2);
|
||||
AddAssert("search text selected", () => searchTextBox.SelectedText == searchTextBox.Current.Value);
|
||||
|
@ -14,7 +14,9 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.Select.Details;
|
||||
using osuTK.Graphics;
|
||||
@ -38,6 +40,12 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
Width = 500
|
||||
});
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("reset game ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo);
|
||||
}
|
||||
|
||||
private BeatmapInfo exampleBeatmapInfo => new BeatmapInfo
|
||||
{
|
||||
Ruleset = rulesets.AvailableRulesets.First(),
|
||||
@ -66,8 +74,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManiaFirstBarText()
|
||||
public void TestManiaFirstBarTextManiaBeatmap()
|
||||
{
|
||||
AddStep("set game ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
||||
|
||||
AddStep("set beatmap", () => advancedStats.BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
Ruleset = rulesets.GetRuleset(3) ?? throw new InvalidOperationException("osu!mania ruleset not found"),
|
||||
@ -84,6 +94,27 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddAssert("first bar text is correct", () => advancedStats.ChildrenOfType<SpriteText>().First().Text == BeatmapsetsStrings.ShowStatsCsMania);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManiaFirstBarTextConvert()
|
||||
{
|
||||
AddStep("set game ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
||||
|
||||
AddStep("set beatmap", () => advancedStats.BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
CircleSize = 5,
|
||||
DrainRate = 4.3f,
|
||||
OverallDifficulty = 4.5f,
|
||||
ApproachRate = 3.1f
|
||||
},
|
||||
StarRating = 8
|
||||
});
|
||||
|
||||
AddAssert("first bar text is correct", () => advancedStats.ChildrenOfType<SpriteText>().First().Text == BeatmapsetsStrings.ShowStatsCsMania);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEasyMod()
|
||||
{
|
||||
|
@ -22,7 +22,6 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Overlays;
|
||||
@ -614,7 +613,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo != null);
|
||||
|
||||
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = "nonono");
|
||||
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<FilterControl.FilterControlTextBox>().First().Text = "nonono");
|
||||
|
||||
AddUntilStep("dummy selected", () => Beatmap.Value is DummyWorkingBeatmap);
|
||||
|
||||
@ -648,7 +647,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddUntilStep("carousel has correct", () => songSelect!.Carousel.SelectedBeatmapInfo?.MatchesOnlineID(target) == true);
|
||||
AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(target));
|
||||
|
||||
AddStep("reset filter text", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = string.Empty);
|
||||
AddStep("reset filter text", () => songSelect!.FilterControl.ChildrenOfType<FilterControl.FilterControlTextBox>().First().Text = string.Empty);
|
||||
|
||||
AddAssert("game still correct", () => Beatmap.Value?.BeatmapInfo.MatchesOnlineID(target) == true);
|
||||
AddAssert("carousel still correct", () => songSelect!.Carousel.SelectedBeatmapInfo.MatchesOnlineID(target));
|
||||
@ -666,7 +665,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo != null);
|
||||
|
||||
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = "nonono");
|
||||
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<FilterControl.FilterControlTextBox>().First().Text = "nonono");
|
||||
|
||||
AddUntilStep("dummy selected", () => Beatmap.Value is DummyWorkingBeatmap);
|
||||
|
||||
@ -689,7 +688,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddUntilStep("carousel has correct", () => songSelect!.Carousel.SelectedBeatmapInfo?.MatchesOnlineID(target) == true);
|
||||
AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(target));
|
||||
|
||||
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = "nononoo");
|
||||
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<FilterControl.FilterControlTextBox>().First().Text = "nononoo");
|
||||
|
||||
AddUntilStep("game lost selection", () => Beatmap.Value is DummyWorkingBeatmap);
|
||||
AddAssert("carousel lost selection", () => songSelect!.Carousel.SelectedBeatmapInfo == null);
|
||||
@ -1135,7 +1134,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
createSongSelect();
|
||||
|
||||
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = "nonono");
|
||||
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<FilterControl.FilterControlTextBox>().First().Text = "nonono");
|
||||
AddStep("select all", () => InputManager.Keys(PlatformAction.SelectAll));
|
||||
AddStep("press ctrl-x", () =>
|
||||
{
|
||||
@ -1144,7 +1143,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
|
||||
AddAssert("filter text cleared", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text, () => Is.Empty);
|
||||
AddAssert("filter text cleared", () => songSelect!.FilterControl.ChildrenOfType<FilterControl.FilterControlTextBox>().First().Text, () => Is.Empty);
|
||||
}
|
||||
|
||||
private void waitForInitialSelection()
|
||||
|
@ -1,9 +1,17 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.States;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
@ -13,8 +21,29 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
new OsuEnumDropdown<BeatmapOnlineStatus>
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Width = 150
|
||||
};
|
||||
|
||||
[Test]
|
||||
// todo: this can be written much better if ThemeComparisonTestScene has a manual input manager
|
||||
public void TestBackAction()
|
||||
{
|
||||
AddStep("open", () => dropdown().ChildrenOfType<Menu>().Single().Open());
|
||||
AddStep("press back", () => dropdown().OnPressed(new KeyBindingPressEvent<GlobalAction>(new InputState(), GlobalAction.Back)));
|
||||
AddAssert("closed", () => dropdown().ChildrenOfType<Menu>().Single().State == MenuState.Closed);
|
||||
|
||||
AddStep("open", () => dropdown().ChildrenOfType<Menu>().Single().Open());
|
||||
AddStep("type something", () => dropdown().ChildrenOfType<DropdownSearchBar>().Single().SearchTerm.Value = "something");
|
||||
AddAssert("search bar visible", () => dropdown().ChildrenOfType<DropdownSearchBar>().Single().State.Value == Visibility.Visible);
|
||||
AddStep("press back", () => dropdown().OnPressed(new KeyBindingPressEvent<GlobalAction>(new InputState(), GlobalAction.Back)));
|
||||
AddAssert("text clear", () => dropdown().ChildrenOfType<DropdownSearchBar>().Single().SearchTerm.Value == string.Empty);
|
||||
AddAssert("search bar hidden", () => dropdown().ChildrenOfType<DropdownSearchBar>().Single().State.Value == Visibility.Hidden);
|
||||
AddAssert("still open", () => dropdown().ChildrenOfType<Menu>().Single().State == MenuState.Open);
|
||||
AddStep("press back", () => dropdown().OnPressed(new KeyBindingPressEvent<GlobalAction>(new InputState(), GlobalAction.Back)));
|
||||
AddAssert("closed", () => dropdown().ChildrenOfType<Menu>().Single().State == MenuState.Closed);
|
||||
|
||||
OsuEnumDropdown<BeatmapOnlineStatus> dropdown() => this.ChildrenOfType<OsuEnumDropdown<BeatmapOnlineStatus>>().First();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -21,6 +20,7 @@ using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Screens.Play;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game
|
||||
{
|
||||
@ -68,6 +68,7 @@ namespace osu.Game
|
||||
|
||||
checkForOutdatedStarRatings();
|
||||
processBeatmapSetsWithMissingMetrics();
|
||||
processBeatmapsWithMissingObjectCounts();
|
||||
processScoresWithMissingStatistics();
|
||||
convertLegacyTotalScoreToStandardised();
|
||||
}, TaskCreationOptions.LongRunning).ContinueWith(t =>
|
||||
@ -134,19 +135,13 @@ namespace osu.Game
|
||||
// of other possible ways), but for now avoid queueing if the user isn't logged in at startup.
|
||||
if (api.IsLoggedIn)
|
||||
{
|
||||
foreach (var b in r.All<BeatmapInfo>().Where(b => b.StarRating < 0 || (b.OnlineID > 0 && b.LastOnlineUpdate == null)))
|
||||
{
|
||||
Debug.Assert(b.BeatmapSet != null);
|
||||
beatmapSetIds.Add(b.BeatmapSet.ID);
|
||||
}
|
||||
foreach (var b in r.All<BeatmapInfo>().Where(b => (b.StarRating < 0 || (b.OnlineID > 0 && b.LastOnlineUpdate == null)) && b.BeatmapSet != null))
|
||||
beatmapSetIds.Add(b.BeatmapSet!.ID);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var b in r.All<BeatmapInfo>().Where(b => b.StarRating < 0))
|
||||
{
|
||||
Debug.Assert(b.BeatmapSet != null);
|
||||
beatmapSetIds.Add(b.BeatmapSet.ID);
|
||||
}
|
||||
foreach (var b in r.All<BeatmapInfo>().Where(b => b.StarRating < 0 && b.BeatmapSet != null))
|
||||
beatmapSetIds.Add(b.BeatmapSet!.ID);
|
||||
}
|
||||
});
|
||||
|
||||
@ -178,6 +173,42 @@ namespace osu.Game
|
||||
}
|
||||
}
|
||||
|
||||
private void processBeatmapsWithMissingObjectCounts()
|
||||
{
|
||||
Logger.Log("Querying for beatmaps with missing hitobject counts to reprocess...");
|
||||
|
||||
HashSet<Guid> beatmapIds = realmAccess.Run(r => new HashSet<Guid>(r.All<BeatmapInfo>()
|
||||
.Filter($"{nameof(BeatmapInfo.Difficulty)}.{nameof(BeatmapDifficulty.TotalObjectCount)} == 0")
|
||||
.AsEnumerable().Select(b => b.ID)));
|
||||
|
||||
Logger.Log($"Found {beatmapIds.Count} beatmaps which require reprocessing.");
|
||||
|
||||
int i = 0;
|
||||
|
||||
foreach (var id in beatmapIds)
|
||||
{
|
||||
sleepIfRequired();
|
||||
|
||||
realmAccess.Run(r =>
|
||||
{
|
||||
var beatmap = r.Find<BeatmapInfo>(id);
|
||||
|
||||
if (beatmap != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Log($"Background processing {beatmap} ({++i} / {beatmapIds.Count})");
|
||||
beatmapUpdater.ProcessObjectCounts(beatmap);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log($"Background processing failed on {beatmap}: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void processScoresWithMissingStatistics()
|
||||
{
|
||||
HashSet<Guid> scoreIds = new HashSet<Guid>();
|
||||
|
@ -21,6 +21,9 @@ namespace osu.Game.Beatmaps
|
||||
public double SliderMultiplier { get; set; } = 1.4;
|
||||
public double SliderTickRate { get; set; } = 1;
|
||||
|
||||
public int EndTimeObjectCount { get; set; }
|
||||
public int TotalObjectCount { get; set; }
|
||||
|
||||
public BeatmapDifficulty()
|
||||
{
|
||||
}
|
||||
@ -44,6 +47,9 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
difficulty.SliderMultiplier = SliderMultiplier;
|
||||
difficulty.SliderTickRate = SliderTickRate;
|
||||
|
||||
difficulty.EndTimeObjectCount = EndTimeObjectCount;
|
||||
difficulty.TotalObjectCount = TotalObjectCount;
|
||||
}
|
||||
|
||||
public virtual void CopyFrom(IBeatmapDifficultyInfo other)
|
||||
@ -55,6 +61,9 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
SliderMultiplier = other.SliderMultiplier;
|
||||
SliderTickRate = other.SliderTickRate;
|
||||
|
||||
EndTimeObjectCount = other.EndTimeObjectCount;
|
||||
TotalObjectCount = other.TotalObjectCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ using osu.Game.IO;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
@ -388,6 +389,8 @@ namespace osu.Game.Beatmaps
|
||||
ApproachRate = decodedDifficulty.ApproachRate,
|
||||
SliderMultiplier = decodedDifficulty.SliderMultiplier,
|
||||
SliderTickRate = decodedDifficulty.SliderTickRate,
|
||||
EndTimeObjectCount = decoded.HitObjects.Count(h => h is IHasDuration),
|
||||
TotalObjectCount = decoded.HitObjects.Count
|
||||
};
|
||||
|
||||
var metadata = new BeatmapMetadata
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Logging;
|
||||
@ -10,6 +11,7 @@ using osu.Framework.Platform;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
@ -44,7 +46,8 @@ namespace osu.Game.Beatmaps
|
||||
public void Queue(Live<BeatmapSetInfo> beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
|
||||
{
|
||||
Logger.Log($"Queueing change for local beatmap {beatmapSet}");
|
||||
Task.Factory.StartNew(() => beatmapSet.PerformRead(b => Process(b, lookupScope)), default, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
|
||||
Task.Factory.StartNew(() => beatmapSet.PerformRead(b => Process(b, lookupScope)), default, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously,
|
||||
updateScheduler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -80,6 +83,21 @@ namespace osu.Game.Beatmaps
|
||||
workingBeatmapCache.Invalidate(beatmapSet);
|
||||
});
|
||||
|
||||
public void ProcessObjectCounts(BeatmapInfo beatmapInfo, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapInfo.Realm!.Write(_ =>
|
||||
{
|
||||
// Before we use below, we want to invalidate.
|
||||
workingBeatmapCache.Invalidate(beatmapInfo);
|
||||
|
||||
var working = workingBeatmapCache.GetWorkingBeatmap(beatmapInfo);
|
||||
var beatmap = working.Beatmap;
|
||||
|
||||
beatmapInfo.Difficulty.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration);
|
||||
beatmapInfo.Difficulty.TotalObjectCount = beatmap.HitObjects.Count;
|
||||
|
||||
// And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
|
||||
workingBeatmapCache.Invalidate(beatmapInfo);
|
||||
});
|
||||
|
||||
#region Implementation of IDisposable
|
||||
|
||||
public void Dispose()
|
||||
|
@ -44,6 +44,19 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
double SliderTickRate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of hitobjects in the beatmap with a distinct end time.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Canonically, these are hitobjects are either sliders or spinners.
|
||||
/// </remarks>
|
||||
int EndTimeObjectCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The total number of hitobjects in the beatmap.
|
||||
/// </summary>
|
||||
int TotalObjectCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Maps a difficulty value [0, 10] to a two-piece linear range of values.
|
||||
/// </summary>
|
||||
|
@ -88,8 +88,9 @@ namespace osu.Game.Database
|
||||
/// 34 2023-08-21 Add BackgroundReprocessingFailed flag to ScoreInfo to track upgrade failures.
|
||||
/// 35 2023-10-16 Clear key combinations of keybindings that are assigned to more than one action in a given settings section.
|
||||
/// 36 2023-10-26 Add LegacyOnlineID to ScoreInfo. Move osu_scores_*_high IDs stored in OnlineID to LegacyOnlineID. Reset anomalous OnlineIDs.
|
||||
/// 37 2023-12-10 Add EndTimeObjectCount and TotalObjectCount to BeatmapDifficulty.
|
||||
/// </summary>
|
||||
private const int schema_version = 36;
|
||||
private const int schema_version = 37;
|
||||
|
||||
/// <summary>
|
||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Database
|
||||
if (score.IsLegacyScore)
|
||||
return false;
|
||||
|
||||
if (score.TotalScoreVersion > 30000002)
|
||||
if (score.TotalScoreVersion > 30000004)
|
||||
return false;
|
||||
|
||||
// Recalculate the old-style standardised score to see if this was an old lazer score.
|
||||
@ -249,14 +249,15 @@ namespace osu.Game.Database
|
||||
int maximumLegacyAccuracyScore = attributes.AccuracyScore;
|
||||
long maximumLegacyComboScore = (long)Math.Round(attributes.ComboScore * legacyModMultiplier);
|
||||
double maximumLegacyBonusRatio = attributes.BonusScoreRatio;
|
||||
long maximumLegacyBonusScore = attributes.BonusScore;
|
||||
|
||||
// The part of total score that doesn't include bonus.
|
||||
double legacyAccScore = maximumLegacyAccuracyScore * score.Accuracy;
|
||||
// We can not separate the ComboScore from the BonusScore, so we keep the bonus in the ratio.
|
||||
double comboProportion =
|
||||
((double)score.LegacyTotalScore - legacyAccScore) / (maximumLegacyComboScore + maximumLegacyBonusScore);
|
||||
|
||||
// We assume the bonus proportion only makes up the rest of the score that exceeds maximumLegacyBaseScore.
|
||||
long maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore;
|
||||
|
||||
// The combo proportion is calculated as a proportion of maximumLegacyBaseScore.
|
||||
double comboProportion = Math.Min(1, (double)score.LegacyTotalScore / maximumLegacyBaseScore);
|
||||
|
||||
// The bonus proportion makes up the rest of the score that exceeds maximumLegacyBaseScore.
|
||||
double bonusProportion = Math.Max(0, ((long)score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio);
|
||||
|
||||
double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n);
|
||||
@ -264,9 +265,92 @@ namespace osu.Game.Database
|
||||
switch (score.Ruleset.OnlineID)
|
||||
{
|
||||
case 0:
|
||||
if (score.MaxCombo == 0 || score.Accuracy == 0)
|
||||
{
|
||||
return (long)Math.Round((
|
||||
0
|
||||
+ 500000 * Math.Pow(score.Accuracy, 5)
|
||||
+ bonusProportion) * modMultiplier);
|
||||
}
|
||||
|
||||
// Assumptions:
|
||||
// - sliders and slider ticks are uniformly distributed in the beatmap, and thus can be ignored without losing much precision.
|
||||
// We thus consider a map of hit-circles only, which gives objectCount == maximumCombo.
|
||||
// - the Ok/Meh hit results are uniformly spread in the score, and thus can be ignored without losing much precision.
|
||||
// We simplify and consider each hit result to have the same hit value of `300 * score.Accuracy`
|
||||
// (which represents the average hit value over the entire play),
|
||||
// which allows us to isolate the accuracy multiplier.
|
||||
|
||||
// This is a very ballpark estimate of the maximum magnitude of the combo portion in score V1.
|
||||
// It is derived by assuming a full combo play and summing up the contribution to combo portion from each individual object.
|
||||
// Because each object's combo contribution is proportional to the current combo at the time of judgement,
|
||||
// this can be roughly represented by summing / integrating f(combo) = combo.
|
||||
// All mod- and beatmap-dependent multipliers and constants are not included here,
|
||||
// as we will only be using the magnitude of this to compute ratios.
|
||||
int maximumLegacyCombo = attributes.MaxCombo;
|
||||
double maximumAchievableComboPortionInScoreV1 = Math.Pow(maximumLegacyCombo, 2);
|
||||
// Similarly, estimate the maximum magnitude of the combo portion in standardised score.
|
||||
// Roughly corresponds to integrating f(combo) = combo ^ COMBO_EXPONENT (omitting constants)
|
||||
double maximumAchievableComboPortionInStandardisedScore = Math.Pow(maximumLegacyCombo, 1 + ScoreProcessor.COMBO_EXPONENT);
|
||||
|
||||
double comboPortionInScoreV1 = maximumAchievableComboPortionInScoreV1 * comboProportion / score.Accuracy;
|
||||
|
||||
// This is - roughly - how much score, in the combo portion, the longest combo on this particular play would gain in score V1.
|
||||
double comboPortionFromLongestComboInScoreV1 = Math.Pow(score.MaxCombo, 2);
|
||||
// Same for standardised score.
|
||||
double comboPortionFromLongestComboInStandardisedScore = Math.Pow(score.MaxCombo, 1 + ScoreProcessor.COMBO_EXPONENT);
|
||||
|
||||
// Calculate how many times the longest combo the user has achieved in the play can repeat
|
||||
// without exceeding the combo portion in score V1 as achieved by the player.
|
||||
// This is a pessimistic estimate; it intentionally does not operate on object count and uses only score instead.
|
||||
double maximumOccurrencesOfLongestCombo = Math.Floor(comboPortionInScoreV1 / comboPortionFromLongestComboInScoreV1);
|
||||
double comboPortionFromRepeatedLongestCombosInScoreV1 = maximumOccurrencesOfLongestCombo * comboPortionFromLongestComboInScoreV1;
|
||||
|
||||
double remainingComboPortionInScoreV1 = comboPortionInScoreV1 - comboPortionFromRepeatedLongestCombosInScoreV1;
|
||||
// `remainingComboPortionInScoreV1` is in the "score ballpark" realm, which means it's proportional to combo squared.
|
||||
// To convert that back to a raw combo length, we need to take the square root...
|
||||
double remainingCombo = Math.Sqrt(remainingComboPortionInScoreV1);
|
||||
// ...and then based on that raw combo length, we calculate how much this last combo is worth in standardised score.
|
||||
double remainingComboPortionInStandardisedScore = Math.Pow(remainingCombo, 1 + ScoreProcessor.COMBO_EXPONENT);
|
||||
|
||||
double lowerEstimateOfComboPortionInStandardisedScore
|
||||
= maximumOccurrencesOfLongestCombo * comboPortionFromLongestComboInStandardisedScore
|
||||
+ remainingComboPortionInStandardisedScore;
|
||||
|
||||
// Compute approximate upper estimate new score for that play.
|
||||
// This time, divide the remaining combo among remaining objects equally to achieve longest possible combo lengths.
|
||||
// There is no rigorous proof that doing this will yield a correct upper bound, but it seems to work out in practice.
|
||||
remainingComboPortionInScoreV1 = comboPortionInScoreV1 - comboPortionFromLongestComboInScoreV1;
|
||||
double remainingCountOfObjectsGivingCombo = maximumLegacyCombo - score.MaxCombo - score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
// Because we assumed all combos were equal, `remainingComboPortionInScoreV1`
|
||||
// can be approximated by n * x^2, wherein n is the assumed number of equal combos,
|
||||
// and x is the assumed length of every one of those combos.
|
||||
// The remaining count of objects giving combo is, using those terms, equal to n * x.
|
||||
// Therefore, dividing the two will result in x, i.e. the assumed length of the remaining combos.
|
||||
double lengthOfRemainingCombos = remainingCountOfObjectsGivingCombo > 0
|
||||
? remainingComboPortionInScoreV1 / remainingCountOfObjectsGivingCombo
|
||||
: 0;
|
||||
// In standardised scoring, each combo yields a score proportional to combo length to the power 1 + COMBO_EXPONENT.
|
||||
// Using the symbols introduced above, that would be x ^ 1.5 per combo, n times (because there are n assumed equal-length combos).
|
||||
// However, because `remainingCountOfObjectsGivingCombo` - using the symbols introduced above - is assumed to be equal to n * x,
|
||||
// we can skip adding the 1 and just multiply by x ^ 0.5.
|
||||
remainingComboPortionInStandardisedScore = remainingCountOfObjectsGivingCombo * Math.Pow(lengthOfRemainingCombos, ScoreProcessor.COMBO_EXPONENT);
|
||||
|
||||
double upperEstimateOfComboPortionInStandardisedScore = comboPortionFromLongestComboInStandardisedScore + remainingComboPortionInStandardisedScore;
|
||||
|
||||
// Approximate by combining lower and upper estimates.
|
||||
// As the lower-estimate is very pessimistic, we use a 30/70 ratio
|
||||
// and cap it with 1.2 times the middle-point to avoid overestimates.
|
||||
double estimatedComboPortionInStandardisedScore = Math.Min(
|
||||
0.3 * lowerEstimateOfComboPortionInStandardisedScore + 0.7 * upperEstimateOfComboPortionInStandardisedScore,
|
||||
1.2 * (lowerEstimateOfComboPortionInStandardisedScore + upperEstimateOfComboPortionInStandardisedScore) / 2
|
||||
);
|
||||
|
||||
double newComboScoreProportion = estimatedComboPortionInStandardisedScore / maximumAchievableComboPortionInStandardisedScore;
|
||||
|
||||
return (long)Math.Round((
|
||||
700000 * comboProportion
|
||||
+ 300000 * Math.Pow(score.Accuracy, 10)
|
||||
500000 * newComboScoreProportion * score.Accuracy
|
||||
+ 500000 * Math.Pow(score.Accuracy, 5)
|
||||
+ bonusProportion) * modMultiplier);
|
||||
|
||||
case 1:
|
||||
|
@ -286,7 +286,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
|
||||
private IUniformBuffer<TriangleBorderData> borderDataBuffer;
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
protected override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
|
||||
|
@ -228,7 +228,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
|
||||
private IUniformBuffer<TriangleBorderData>? borderDataBuffer;
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
protected override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
|
||||
|
@ -134,7 +134,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
lengths.AddRange(Source.bars.InstantaneousLengths);
|
||||
}
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
protected override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
|
||||
|
@ -22,7 +22,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class OsuDropdown<T> : Dropdown<T>
|
||||
public partial class OsuDropdown<T> : Dropdown<T>, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
private const float corner_radius = 5;
|
||||
|
||||
@ -30,9 +30,23 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
protected override DropdownMenu CreateMenu() => new OsuDropdownMenu();
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (e.Repeat) return false;
|
||||
|
||||
if (e.Action == GlobalAction.Back)
|
||||
return Back();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||
{
|
||||
}
|
||||
|
||||
#region OsuDropdownMenu
|
||||
|
||||
protected partial class OsuDropdownMenu : DropdownMenu, IKeyBindingHandler<GlobalAction>
|
||||
protected partial class OsuDropdownMenu : DropdownMenu
|
||||
{
|
||||
public override bool HandleNonPositionalInput => State == MenuState.Open;
|
||||
|
||||
@ -276,23 +290,6 @@ namespace osu.Game.Graphics.UserInterface
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (e.Repeat) return false;
|
||||
|
||||
if (e.Action == GlobalAction.Back)
|
||||
{
|
||||
State = MenuState.Closed;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -355,11 +352,77 @@ namespace osu.Game.Graphics.UserInterface
|
||||
AddInternal(new HoverClickSounds());
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OverlayColourProvider? colourProvider, OsuColour colours)
|
||||
[Resolved]
|
||||
private OverlayColourProvider? colourProvider { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f);
|
||||
BackgroundColourHover = colourProvider?.Light4 ?? colours.PinkDarker;
|
||||
base.LoadComplete();
|
||||
|
||||
SearchBar.State.ValueChanged += _ => updateColour();
|
||||
updateColour();
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateColour();
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
updateColour();
|
||||
}
|
||||
|
||||
private void updateColour()
|
||||
{
|
||||
bool hovered = Enabled.Value && IsHovered;
|
||||
var hoveredColour = colourProvider?.Light4 ?? colours.PinkDarker;
|
||||
var unhoveredColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f);
|
||||
|
||||
if (SearchBar.State.Value == Visibility.Visible)
|
||||
{
|
||||
Icon.Colour = hovered ? hoveredColour.Lighten(0.5f) : Colour4.White;
|
||||
Background.Colour = unhoveredColour;
|
||||
}
|
||||
else
|
||||
{
|
||||
Icon.Colour = Color4.White;
|
||||
Background.Colour = hovered ? hoveredColour : unhoveredColour;
|
||||
}
|
||||
}
|
||||
|
||||
protected override DropdownSearchBar CreateSearchBar() => new OsuDropdownSearchBar
|
||||
{
|
||||
Padding = new MarginPadding { Right = 36 },
|
||||
};
|
||||
|
||||
private partial class OsuDropdownSearchBar : DropdownSearchBar
|
||||
{
|
||||
protected override void PopIn() => this.FadeIn();
|
||||
|
||||
protected override void PopOut() => this.FadeOut();
|
||||
|
||||
protected override TextBox CreateTextBox() => new DropdownSearchTextBox
|
||||
{
|
||||
FontSize = OsuFont.Default.Size,
|
||||
};
|
||||
|
||||
private partial class DropdownSearchTextBox : SearchTextBox
|
||||
{
|
||||
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (e.Action == GlobalAction.Back)
|
||||
// this method is blocking Dropdown from receiving the back action, despite this text box residing in a separate input manager.
|
||||
// to fix this properly, a local global action container needs to be added as well, but for simplicity, just don't handle the back action here.
|
||||
return false;
|
||||
|
||||
return base.OnPressed(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Child = new PasswordMaskChar(CalculatedTextSize),
|
||||
Child = new PasswordMaskChar(FontSize),
|
||||
};
|
||||
|
||||
protected override bool AllowUniqueCharacterSamples => false;
|
||||
|
@ -268,7 +268,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Child = new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) },
|
||||
Child = new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: FontSize) },
|
||||
};
|
||||
|
||||
protected override Caret CreateCaret() => caret = new OsuCaret
|
||||
@ -314,18 +314,16 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
public OsuCaret()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
Size = new Vector2(1, 0.9f);
|
||||
|
||||
Colour = Color4.Transparent;
|
||||
Anchor = Anchor.CentreLeft;
|
||||
Origin = Anchor.CentreLeft;
|
||||
|
||||
Masking = true;
|
||||
CornerRadius = 1;
|
||||
InternalChild = beatSync = new CaretBeatSyncedContainer
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Masking = true,
|
||||
CornerRadius = 1f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.9f,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -221,7 +221,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
tierColours.AddRange(Source.tierColours);
|
||||
}
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
protected override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
|
||||
|
@ -110,7 +110,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
BackgroundFocused = colourProvider.Background4;
|
||||
BackgroundUnfocused = colourProvider.Background4;
|
||||
|
||||
Placeholder.Font = OsuFont.GetFont(size: CalculatedTextSize, weight: FontWeight.SemiBold);
|
||||
Placeholder.Font = OsuFont.GetFont(size: FontSize, weight: FontWeight.SemiBold);
|
||||
PlaceholderText = CommonStrings.InputSearch;
|
||||
|
||||
CornerRadius = corner_radius;
|
||||
|
@ -121,7 +121,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
break;
|
||||
|
||||
default:
|
||||
slider.Current.Parse(textBox.Current.Value);
|
||||
slider.Current.Parse(textBox.Current.Value, CultureInfo.CurrentCulture);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -109,6 +109,8 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
CircleSize = CircleSize,
|
||||
ApproachRate = ApproachRate,
|
||||
OverallDifficulty = OverallDifficulty,
|
||||
EndTimeObjectCount = SliderCount + SpinnerCount,
|
||||
TotalObjectCount = CircleCount + SliderCount + SpinnerCount
|
||||
};
|
||||
|
||||
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
|
||||
|
@ -150,6 +150,12 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Whether this <see cref="ScoreInfo"/> represents a legacy (osu!stable) score.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsLegacyScore => LegacyScoreId != null;
|
||||
|
||||
public override string ToString() => $"score_id: {ID} user_id: {UserID}";
|
||||
|
||||
/// <summary>
|
||||
@ -191,6 +197,7 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
{
|
||||
OnlineID = OnlineID,
|
||||
LegacyOnlineID = (long?)LegacyScoreId ?? -1,
|
||||
IsLegacyScore = IsLegacyScore,
|
||||
User = User ?? new APIUser { Id = UserID },
|
||||
BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID },
|
||||
Ruleset = new RulesetInfo { OnlineID = RulesetID },
|
||||
|
@ -57,9 +57,11 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
{
|
||||
skinDropdown = new SkinSettingsDropdown
|
||||
{
|
||||
AlwaysShowSearchBar = true,
|
||||
AllowNonContiguousMatching = true,
|
||||
LabelText = SkinSettingsStrings.CurrentSkin,
|
||||
Current = skins.CurrentSkinInfo,
|
||||
Keywords = new[] { @"skins" }
|
||||
Keywords = new[] { @"skins" },
|
||||
},
|
||||
new SettingsButton
|
||||
{
|
||||
|
@ -16,6 +16,18 @@ namespace osu.Game.Overlays.Settings
|
||||
{
|
||||
protected new OsuDropdown<T> Control => (OsuDropdown<T>)base.Control;
|
||||
|
||||
public bool AlwaysShowSearchBar
|
||||
{
|
||||
get => Control.AlwaysShowSearchBar;
|
||||
set => Control.AlwaysShowSearchBar = value;
|
||||
}
|
||||
|
||||
public bool AllowNonContiguousMatching
|
||||
{
|
||||
get => Control.AllowNonContiguousMatching;
|
||||
set => Control.AllowNonContiguousMatching = value;
|
||||
}
|
||||
|
||||
public IEnumerable<T> Items
|
||||
{
|
||||
get => Control.Items;
|
||||
|
@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Configuration
|
||||
|
||||
if (setting != null)
|
||||
{
|
||||
bindable.Parse(setting.Value);
|
||||
bindable.Parse(setting.Value, CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
new CheckFewHitsounds(),
|
||||
new CheckTooShortAudioFiles(),
|
||||
new CheckAudioInVideo(),
|
||||
new CheckDelayedHitsounds(),
|
||||
|
||||
// Files
|
||||
new CheckZeroByteFiles(),
|
||||
|
181
osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs
Normal file
181
osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs
Normal file
@ -0,0 +1,181 @@
|
||||
// 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 osu.Framework.Audio.Track;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks
|
||||
{
|
||||
public class CheckDelayedHitsounds : ICheck
|
||||
{
|
||||
/// <summary>
|
||||
/// Threshold at which point the sample is considered silent.
|
||||
/// </summary>
|
||||
private const float silence_threshold = 0.001f;
|
||||
|
||||
private const float falloff_factor = 0.95f;
|
||||
private const int delay_threshold = 5;
|
||||
private const int delay_threshold_negligible = 1;
|
||||
|
||||
public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Delayed hit sounds.");
|
||||
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||
{
|
||||
new IssueTemplateConsequentDelay(this),
|
||||
new IssueTemplateDelay(this),
|
||||
new IssueTemplateDelayNoSilence(this),
|
||||
new IssueTemplateMinorDelay(this),
|
||||
new IssueTemplateMinorDelayNoSilence(this),
|
||||
};
|
||||
|
||||
private float getAverageAmplitude(Waveform.Point point) => (point.AmplitudeLeft + point.AmplitudeRight) / 2;
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet;
|
||||
|
||||
if (beatmapSet == null)
|
||||
yield break;
|
||||
|
||||
foreach (var file in beatmapSet.Files)
|
||||
{
|
||||
using (Stream? stream = context.WorkingBeatmap.GetStream(file.File.GetStoragePath()))
|
||||
{
|
||||
if (stream == null)
|
||||
continue;
|
||||
|
||||
if (!isHitSound(file.Filename))
|
||||
continue;
|
||||
|
||||
using Waveform waveform = new Waveform(stream);
|
||||
|
||||
var points = waveform.GetPoints();
|
||||
|
||||
// Skip muted samples
|
||||
if (points.Length == 0 || points.Sum(getAverageAmplitude) <= silence_threshold)
|
||||
continue;
|
||||
|
||||
float maxAmplitude = points.Select(getAverageAmplitude).Max();
|
||||
|
||||
int consequentDelay = 0;
|
||||
int delay = 0;
|
||||
float amplitude = 0;
|
||||
|
||||
while (delay + consequentDelay < points.Length)
|
||||
{
|
||||
amplitude += getAverageAmplitude(points[delay]);
|
||||
|
||||
// Reached peak amplitude/transient
|
||||
if (amplitude >= maxAmplitude)
|
||||
break;
|
||||
|
||||
amplitude *= falloff_factor;
|
||||
|
||||
if (amplitude < silence_threshold)
|
||||
{
|
||||
amplitude = 0;
|
||||
consequentDelay++;
|
||||
}
|
||||
|
||||
delay++;
|
||||
}
|
||||
|
||||
if (consequentDelay >= delay_threshold)
|
||||
yield return new IssueTemplateConsequentDelay(this).Create(file.Filename, consequentDelay);
|
||||
else if (consequentDelay + delay >= delay_threshold)
|
||||
{
|
||||
if (consequentDelay > 0)
|
||||
yield return new IssueTemplateDelay(this).Create(file.Filename, consequentDelay, delay);
|
||||
else
|
||||
yield return new IssueTemplateDelayNoSilence(this).Create(file.Filename, delay);
|
||||
}
|
||||
else if (consequentDelay + delay >= delay_threshold_negligible)
|
||||
{
|
||||
if (consequentDelay > 0)
|
||||
yield return new IssueTemplateMinorDelay(this).Create(file.Filename, consequentDelay, delay);
|
||||
else
|
||||
yield return new IssueTemplateMinorDelayNoSilence(this).Create(file.Filename, delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool isHitSound(string filename)
|
||||
{
|
||||
if (!AudioCheckUtils.HasAudioExtension(filename))
|
||||
return false;
|
||||
|
||||
// <bank>-<sampleset>
|
||||
string[] parts = filename.ToLowerInvariant().Split('-');
|
||||
|
||||
if (parts.Length != 2)
|
||||
return false;
|
||||
|
||||
string bank = parts[0];
|
||||
string sampleSet = parts[1];
|
||||
|
||||
return HitSampleInfo.AllBanks.Contains(bank)
|
||||
&& HitSampleInfo.AllAdditions.Append(HitSampleInfo.HIT_NORMAL).Any(sampleSet.StartsWith);
|
||||
}
|
||||
|
||||
public class IssueTemplateConsequentDelay : IssueTemplate
|
||||
{
|
||||
public IssueTemplateConsequentDelay(ICheck check)
|
||||
: base(check, IssueType.Problem,
|
||||
"\"{0}\" has a {1:0.##} ms period of complete silence at the start.")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(string filename, int pureDelay) => new Issue(this, filename, pureDelay);
|
||||
}
|
||||
|
||||
public class IssueTemplateDelay : IssueTemplate
|
||||
{
|
||||
public IssueTemplateDelay(ICheck check)
|
||||
: base(check, IssueType.Warning,
|
||||
"\"{0}\" has a transient delay of ~{1:0.##} ms, of which {2:0.##} ms is complete silence.")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(string filename, int consequentDelay, int delay) => new Issue(this, filename, delay, consequentDelay);
|
||||
}
|
||||
|
||||
public class IssueTemplateDelayNoSilence : IssueTemplate
|
||||
{
|
||||
public IssueTemplateDelayNoSilence(ICheck check)
|
||||
: base(check, IssueType.Warning,
|
||||
"\"{0}\" has a transient delay of ~{1:0.##} ms.")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(string filename, int delay) => new Issue(this, filename, delay);
|
||||
}
|
||||
|
||||
public class IssueTemplateMinorDelay : IssueTemplate
|
||||
{
|
||||
public IssueTemplateMinorDelay(ICheck check)
|
||||
: base(check, IssueType.Negligible,
|
||||
"\"{0}\" has a transient delay of ~{1:0.##} ms, of which {2:0.##} ms is complete silence.")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(string filename, int consequentDelay, int delay) => new Issue(this, filename, delay, consequentDelay);
|
||||
}
|
||||
|
||||
public class IssueTemplateMinorDelayNoSilence : IssueTemplate
|
||||
{
|
||||
public IssueTemplateMinorDelayNoSilence(ICheck check)
|
||||
: base(check, IssueType.Negligible,
|
||||
"\"{0}\" has a transient delay of ~{1:0.##} ms.")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(string filename, int delay) => new Issue(this, filename, delay);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using ManagedBass;
|
||||
using osu.Framework.Audio.Callbacks;
|
||||
using osu.Game.Extensions;
|
||||
@ -16,8 +15,6 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
private const int ms_threshold = 25;
|
||||
private const int min_bytes_threshold = 100;
|
||||
|
||||
private readonly string[] audioExtensions = { "mp3", "ogg", "wav" };
|
||||
|
||||
public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Too short audio files");
|
||||
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||
@ -46,7 +43,7 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
{
|
||||
// If the file is not likely to be properly parsed by Bass, we don't produce Error issues about it.
|
||||
// Image files and audio files devoid of audio data both fail, for example, but neither would be issues in this check.
|
||||
if (hasAudioExtension(file.Filename) && probablyHasAudioData(data))
|
||||
if (AudioCheckUtils.HasAudioExtension(file.Filename) && probablyHasAudioData(data))
|
||||
yield return new IssueTemplateBadFormat(this).Create(file.Filename);
|
||||
|
||||
continue;
|
||||
@ -63,7 +60,6 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
}
|
||||
}
|
||||
|
||||
private bool hasAudioExtension(string filename) => audioExtensions.Any(filename.ToLowerInvariant().EndsWith);
|
||||
private bool probablyHasAudioData(Stream data) => data.Length > min_bytes_threshold;
|
||||
|
||||
public class IssueTemplateTooShort : IssueTemplate
|
||||
|
15
osu.Game/Rulesets/Edit/Checks/Components/AudioCheckUtils.cs
Normal file
15
osu.Game/Rulesets/Edit/Checks/Components/AudioCheckUtils.cs
Normal file
@ -0,0 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks.Components
|
||||
{
|
||||
public static class AudioCheckUtils
|
||||
{
|
||||
public static readonly string[] AUDIO_EXTENSIONS = { "mp3", "ogg", "wav" };
|
||||
|
||||
public static bool HasAudioExtension(string filename) => AUDIO_EXTENSIONS.Any(Path.GetExtension(filename).ToLowerInvariant().EndsWith);
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
|
||||
namespace osu.Game.Rulesets
|
||||
@ -14,6 +15,12 @@ namespace osu.Game.Rulesets
|
||||
/// </summary>
|
||||
int LegacyID { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the number of mania keys required to play the beatmap.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
int GetKeyCount(IBeatmapInfo beatmapInfo) => 0;
|
||||
|
||||
ILegacyScoreSimulator CreateLegacyScoreSimulator();
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
@ -284,7 +285,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
if (!(target is IParseable parseable))
|
||||
throw new InvalidOperationException($"Bindable type {target.GetType().ReadableName()} is not {nameof(IParseable)}.");
|
||||
|
||||
parseable.Parse(source);
|
||||
parseable.Parse(source, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -252,7 +252,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
private IUniformBuffer<FlashlightParameters>? flashlightParametersBuffer;
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
protected override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
|
||||
|
@ -29,12 +29,12 @@ namespace osu.Game.Rulesets.Scoring.Legacy
|
||||
public float OverallDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The count of hitcircles in the beatmap.
|
||||
/// The number of hitobjects in the beatmap with a distinct end time.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When converting from osu! ruleset beatmaps, this is equivalent to the sum of sliders and spinners in the beatmap.
|
||||
/// Canonically, these are hitobjects are either sliders or spinners.
|
||||
/// </remarks>
|
||||
public int CircleCount { get; set; }
|
||||
public int EndTimeObjectCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total count of hitobjects in the beatmap.
|
||||
@ -46,22 +46,24 @@ namespace osu.Game.Rulesets.Scoring.Legacy
|
||||
double IBeatmapDifficultyInfo.SliderMultiplier => 0;
|
||||
double IBeatmapDifficultyInfo.SliderTickRate => 0;
|
||||
|
||||
public static LegacyBeatmapConversionDifficultyInfo FromAPIBeatmap(APIBeatmap apiBeatmap) => new LegacyBeatmapConversionDifficultyInfo
|
||||
{
|
||||
SourceRuleset = apiBeatmap.Ruleset,
|
||||
CircleSize = apiBeatmap.CircleSize,
|
||||
OverallDifficulty = apiBeatmap.OverallDifficulty,
|
||||
CircleCount = apiBeatmap.CircleCount,
|
||||
TotalObjectCount = apiBeatmap.SliderCount + apiBeatmap.SpinnerCount + apiBeatmap.CircleCount
|
||||
};
|
||||
public static LegacyBeatmapConversionDifficultyInfo FromAPIBeatmap(APIBeatmap apiBeatmap) => FromBeatmapInfo(apiBeatmap);
|
||||
|
||||
public static LegacyBeatmapConversionDifficultyInfo FromBeatmap(IBeatmap beatmap) => new LegacyBeatmapConversionDifficultyInfo
|
||||
{
|
||||
SourceRuleset = beatmap.BeatmapInfo.Ruleset,
|
||||
CircleSize = beatmap.Difficulty.CircleSize,
|
||||
OverallDifficulty = beatmap.Difficulty.OverallDifficulty,
|
||||
CircleCount = beatmap.HitObjects.Count(h => h is not IHasDuration),
|
||||
EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration),
|
||||
TotalObjectCount = beatmap.HitObjects.Count
|
||||
};
|
||||
|
||||
public static LegacyBeatmapConversionDifficultyInfo FromBeatmapInfo(IBeatmapInfo beatmapInfo) => new LegacyBeatmapConversionDifficultyInfo
|
||||
{
|
||||
SourceRuleset = beatmapInfo.Ruleset,
|
||||
CircleSize = beatmapInfo.Difficulty.CircleSize,
|
||||
OverallDifficulty = beatmapInfo.Difficulty.OverallDifficulty,
|
||||
EndTimeObjectCount = beatmapInfo.Difficulty.EndTimeObjectCount,
|
||||
TotalObjectCount = beatmapInfo.Difficulty.TotalObjectCount
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -19,5 +19,15 @@ namespace osu.Game.Rulesets.Scoring.Legacy
|
||||
/// A ratio of standardised score to legacy score for the bonus part of total score.
|
||||
/// </summary>
|
||||
public double BonusScoreRatio;
|
||||
|
||||
/// <summary>
|
||||
/// The bonus portion of the legacy (ScoreV1) total score.
|
||||
/// </summary>
|
||||
public int BonusScore;
|
||||
|
||||
/// <summary>
|
||||
/// The max combo of the legacy (ScoreV1) total score.
|
||||
/// </summary>
|
||||
public int MaxCombo;
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,14 @@ namespace osu.Game.Rulesets.Scoring
|
||||
{
|
||||
public partial class ScoreProcessor : JudgementProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// The exponent applied to combo in the default implementation of <see cref="GetComboScoreChange"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If a custom implementation overrides <see cref="GetComboScoreChange"/> this may not be relevant.
|
||||
/// </remarks>
|
||||
public const double COMBO_EXPONENT = 0.5;
|
||||
|
||||
public const double MAX_SCORE = 1000000;
|
||||
|
||||
private const double accuracy_cutoff_x = 1;
|
||||
@ -210,9 +218,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1;
|
||||
|
||||
if (!result.Type.IsScorable())
|
||||
return;
|
||||
|
||||
if (result.Type.IncreasesCombo())
|
||||
Combo.Value++;
|
||||
else if (result.Type.BreaksCombo())
|
||||
@ -220,16 +225,18 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
result.ComboAfterJudgement = Combo.Value;
|
||||
|
||||
if (result.Type.AffectsAccuracy())
|
||||
if (result.Judgement.MaxResult.AffectsAccuracy())
|
||||
{
|
||||
currentMaximumBaseScore += Judgement.ToNumericResult(result.Judgement.MaxResult);
|
||||
currentBaseScore += Judgement.ToNumericResult(result.Type);
|
||||
currentAccuracyJudgementCount++;
|
||||
}
|
||||
|
||||
if (result.Type.AffectsAccuracy())
|
||||
currentBaseScore += Judgement.ToNumericResult(result.Type);
|
||||
|
||||
if (result.Type.IsBonus())
|
||||
currentBonusPortion += GetBonusScoreChange(result);
|
||||
else
|
||||
else if (result.Type.IsScorable())
|
||||
currentComboPortion += GetComboScoreChange(result);
|
||||
|
||||
ApplyScoreChange(result);
|
||||
@ -267,19 +274,18 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1;
|
||||
|
||||
if (!result.Type.IsScorable())
|
||||
return;
|
||||
|
||||
if (result.Type.AffectsAccuracy())
|
||||
if (result.Judgement.MaxResult.AffectsAccuracy())
|
||||
{
|
||||
currentMaximumBaseScore -= Judgement.ToNumericResult(result.Judgement.MaxResult);
|
||||
currentBaseScore -= Judgement.ToNumericResult(result.Type);
|
||||
currentAccuracyJudgementCount--;
|
||||
}
|
||||
|
||||
if (result.Type.AffectsAccuracy())
|
||||
currentBaseScore -= Judgement.ToNumericResult(result.Type);
|
||||
|
||||
if (result.Type.IsBonus())
|
||||
currentBonusPortion -= GetBonusScoreChange(result);
|
||||
else
|
||||
else if (result.Type.IsScorable())
|
||||
currentComboPortion -= GetComboScoreChange(result);
|
||||
|
||||
RemoveScoreChange(result);
|
||||
@ -293,7 +299,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
protected virtual double GetBonusScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Type);
|
||||
|
||||
protected virtual double GetComboScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Type) * (1 + result.ComboAfterJudgement / 10d);
|
||||
protected virtual double GetComboScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Judgement.MaxResult) * Math.Pow(result.ComboAfterJudgement, COMBO_EXPONENT);
|
||||
|
||||
protected virtual void ApplyScoreChange(JudgementResult result)
|
||||
{
|
||||
@ -317,8 +323,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
protected virtual double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
|
||||
{
|
||||
return 700000 * comboProgress +
|
||||
300000 * Math.Pow(Accuracy.Value, 10) * accuracyProgress +
|
||||
return 500000 * Accuracy.Value * comboProgress +
|
||||
500000 * Math.Pow(Accuracy.Value, 5) * accuracyProgress +
|
||||
bonusPortion;
|
||||
}
|
||||
|
||||
|
@ -31,9 +31,10 @@ namespace osu.Game.Scoring.Legacy
|
||||
/// <item><description>30000002: Score stored to replay calculated using the Score V2 algorithm. Legacy scores on this version are candidate to Score V1 -> V2 conversion.</description></item>
|
||||
/// <item><description>30000003: First version after converting legacy total score to standardised.</description></item>
|
||||
/// <item><description>30000004: Fixed mod multipliers during legacy score conversion. Reconvert all scores.</description></item>
|
||||
/// <item><description>30000005: Introduce combo exponent in the osu! gamemode. Reconvert all scores.</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public const int LATEST_VERSION = 30000004;
|
||||
public const int LATEST_VERSION = 30000005;
|
||||
|
||||
/// <summary>
|
||||
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
|
||||
|
@ -103,7 +103,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
break;
|
||||
|
||||
default:
|
||||
slider.Current.Parse(t.Text);
|
||||
slider.Current.Parse(t.Text, CultureInfo.CurrentCulture);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -189,7 +189,7 @@ namespace osu.Game.Screens.Menu
|
||||
Source.frequencyAmplitudes.AsSpan().CopyTo(audioData);
|
||||
}
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
protected override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
|
||||
|
@ -23,11 +23,11 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
AccuracyDisplay.BindValueChanged(mod =>
|
||||
AccuracyDisplay.BindValueChanged(mode =>
|
||||
{
|
||||
Current.UnbindBindings();
|
||||
|
||||
switch (mod.NewValue)
|
||||
switch (mode.NewValue)
|
||||
{
|
||||
case AccuracyDisplayMode.Standard:
|
||||
Current.BindTo(scoreProcessor.Accuracy);
|
||||
|
@ -27,6 +27,7 @@ using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -57,6 +58,8 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
private StarCounter starCounter = null!;
|
||||
private DifficultyIcon difficultyIcon = null!;
|
||||
|
||||
private OsuSpriteText keyCountText = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapSetOverlay? beatmapOverlay { get; set; }
|
||||
|
||||
@ -69,6 +72,9 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
private IBindable<StarDifficulty?> starDifficultyBindable = null!;
|
||||
private CancellationTokenSource? starDifficultyCancellationSource;
|
||||
|
||||
@ -133,6 +139,13 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
{
|
||||
keyCountText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 20),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Alpha = 0,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = beatmapInfo.DifficultyName,
|
||||
@ -167,6 +180,13 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ruleset.BindValueChanged(_ => updateKeyCount());
|
||||
}
|
||||
|
||||
protected override void Selected()
|
||||
{
|
||||
base.Selected();
|
||||
@ -216,11 +236,28 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
if (d.NewValue != null)
|
||||
difficultyIcon.Current.Value = d.NewValue.Value;
|
||||
}, true);
|
||||
|
||||
updateKeyCount();
|
||||
}
|
||||
|
||||
base.ApplyState();
|
||||
}
|
||||
|
||||
private void updateKeyCount()
|
||||
{
|
||||
if (ruleset.Value.OnlineID == 3)
|
||||
{
|
||||
// Account for mania differences locally for now.
|
||||
// Eventually this should be handled in a more modular way, allowing rulesets to add more information to the panel.
|
||||
ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance();
|
||||
|
||||
keyCountText.Alpha = 1;
|
||||
keyCountText.Text = $"[{legacyRuleset.GetKeyCount(beatmapInfo)}K]";
|
||||
}
|
||||
else
|
||||
keyCountText.Alpha = 0;
|
||||
}
|
||||
|
||||
public MenuItem[] ContextMenuItems
|
||||
{
|
||||
get
|
||||
|
@ -153,13 +153,23 @@ namespace osu.Game.Screens.Select.Details
|
||||
}
|
||||
}
|
||||
|
||||
switch (BeatmapInfo?.Ruleset.OnlineID)
|
||||
IRulesetInfo ruleset = gameRuleset?.Value ?? beatmapInfo.Ruleset;
|
||||
|
||||
switch (ruleset.OnlineID)
|
||||
{
|
||||
case 3:
|
||||
// Account for mania differences locally for now
|
||||
// Eventually this should be handled in a more modular way, allowing rulesets to return arbitrary difficulty attributes
|
||||
// Account for mania differences locally for now.
|
||||
// Eventually this should be handled in a more modular way, allowing rulesets to return arbitrary difficulty attributes.
|
||||
ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.CreateInstance();
|
||||
|
||||
// For the time being, the key count is static no matter what, because:
|
||||
// a) The method doesn't have knowledge of the active keymods. Doing so may require considerations for filtering.
|
||||
// b) Using the difficulty adjustment mod to adjust OD doesn't have an effect on conversion.
|
||||
int keyCount = baseDifficulty == null ? 0 : legacyRuleset.GetKeyCount(BeatmapInfo);
|
||||
|
||||
FirstValue.Title = BeatmapsetsStrings.ShowStatsCsMania;
|
||||
FirstValue.Value = (baseDifficulty?.CircleSize ?? 0, null);
|
||||
FirstValue.Value = (keyCount, keyCount);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -250,7 +250,7 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
protected override bool OnHover(HoverEvent e) => true;
|
||||
|
||||
private partial class FilterControlTextBox : SeekLimitedSearchTextBox
|
||||
internal partial class FilterControlTextBox : SeekLimitedSearchTextBox
|
||||
{
|
||||
private const float filter_text_size = 12;
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
@ -46,7 +47,7 @@ namespace osu.Game.Skinning
|
||||
if (!(target is IParseable parseable))
|
||||
throw new InvalidOperationException($"Bindable type {target.GetType().ReadableName()} is not {nameof(IParseable)}.");
|
||||
|
||||
parseable.Parse(source);
|
||||
parseable.Parse(source, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
@ -330,7 +331,7 @@ namespace osu.Game.Skinning
|
||||
|
||||
var bindable = new Bindable<TValue>();
|
||||
if (val != null)
|
||||
bindable.Parse(val);
|
||||
bindable.Parse(val, CultureInfo.InvariantCulture);
|
||||
return bindable;
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="11.5.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2023.1201.1" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2023.1213.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1127.0" />
|
||||
<PackageReference Include="Sentry" Version="3.40.0" />
|
||||
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||
|
@ -23,6 +23,6 @@
|
||||
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.1201.1" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.1213.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
Loading…
Reference in New Issue
Block a user