1
0
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:
Dean Herbert 2023-12-13 16:35:18 +09:00 committed by GitHub
commit 0259ab761b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 1464 additions and 270 deletions

View File

@ -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.

View File

@ -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;
}
}

View File

@ -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;
}
}
}
}

View File

@ -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];

View File

@ -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)

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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));

View File

@ -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
}
}
}

View File

@ -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);
}
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using 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;
}
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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;
}

View 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);
}
}
}

Binary file not shown.

Binary file not shown.

View File

@ -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; }

View File

@ -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);
}
}
}

View File

@ -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]

View File

@ -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";
});

View File

@ -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;

View File

@ -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);

View File

@ -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()
{

View File

@ -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()

View File

@ -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();
}
}
}

View File

@ -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>();

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -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()

View File

@ -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>

View File

@ -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.

View File

@ -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:

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);
}
}
}
}
}

View File

@ -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;

View File

@ -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,
};
}

View File

@ -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);

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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 },

View File

@ -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
{

View File

@ -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;

View File

@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Configuration
if (setting != null)
{
bindable.Parse(setting.Value);
bindable.Parse(setting.Value, CultureInfo.InvariantCulture);
}
else
{

View File

@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Edit
new CheckFewHitsounds(),
new CheckTooShortAudioFiles(),
new CheckAudioInVideo(),
new CheckDelayedHitsounds(),
// Files
new CheckZeroByteFiles(),

View 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);
}
}
}

View File

@ -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

View 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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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
};
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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.

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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:

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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. -->

View File

@ -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>