1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 09:27:29 +08:00

Merge branch 'master' into slider-late-hit-lenience

This commit is contained in:
Dean Herbert 2023-12-17 13:00:01 +09:00 committed by GitHub
commit c0e96927aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 1557 additions and 195 deletions

View File

@ -52,6 +52,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase("3644427", new[] { typeof(CatchModEasy), typeof(CatchModFlashlight) })]
[TestCase("3689906", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
[TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
[TestCase("112643")]
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)

View File

@ -0,0 +1,52 @@
// 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.Beatmaps;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
public class CatchRateAdjustedDisplayDifficultyTest
{
private static IEnumerable<float> difficultyValuesToTest()
{
for (float i = 0; i <= 10; i += 0.5f)
yield return i;
}
[TestCaseSource(nameof(difficultyValuesToTest))]
public void TestApproachRateIsUnchangedWithRateEqualToOne(float originalApproachRate)
{
var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
}
[Test]
public void TestRateBelowOne()
{
var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
}
[Test]
public void TestRateAboveOne()
{
var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,582 @@
osu file format v9
[General]
StackLeniency: 0.7
Mode: 0
[Difficulty]
HPDrainRate:7
CircleSize:5
OverallDifficulty:8
ApproachRate:8
SliderMultiplier:3.2
SliderTickRate:2
[Events]
//Background and Video events
//Break Periods
2,16325,17625
2,32325,33875
2,66325,67375
2,120135,127375
//Storyboard Layer 0 (Background)
//Storyboard Layer 1 (Fail)
//Storyboard Layer 2 (Pass)
//Storyboard Layer 3 (Foreground)
//Storyboard Sound Samples
//Background Colour Transformations
3,100,163,162,255
[TimingPoints]
125,500,4,1,0,50,1,0
36125,-100,4,1,0,50,0,1
66125,-100,4,1,0,50,0,0
88125,-100,4,1,0,50,0,1
120125,-100,4,1,0,50,0,0
170125,-100,4,2,0,5,0,0
170250,-100,4,1,0,50,0,0
172125,-100,4,1,0,50,0,1
200125,-100,4,1,0,50,0,0
[HitObjects]
64,80,2375,5,0
172,192,2625,1,2
152,36,2875,1,0
80,176,3125,1,2
224,112,3375,1,0
192,256,3625,1,8
136,116,3875,1,0
272,32,4125,2,2,B|376:0|408:56|412:125|320:144|304:176|328:216|368:272|496:208,1,400,6|0
504,216,4875,2,2,B|376:232|288:280|248:384,1,320
384,344,5625,1,8
272,216,5875,1,0
272,216,6000,1,0
272,216,6125,1,4
92,280,6375,5,0
124,108,6625,1,8
256,8,6875,1,0
388,108,7125,1,2
420,280,7375,1,8
256,296,7625,1,8
256,120,7875,1,0
443,152,8125,2,2,B|397:202|305:219|256:192|203:163|114:181|68:231,1,400,2|0
24,256,8875,2,2,B|112:227|141:134|122:36|37:1,1,320
16,132,9625,1,8
136,280,9875,1,0
136,280,10000,1,0
136,280,10125,1,4
256,172,10375,5,0
368,56,10625,1,8
196,116,10875,1,0
316,116,11125,1,2
144,56,11375,1,0
256,0,11625,1,8
112,128,11875,1,0
164,280,12125,6,0,B|256:316,1,80,4|2
100,348,12500,2,0,B|8:312,1,80,0|2
144,212,12875,2,0,B|52:176,1,80,0|2
208,144,13250,2,0,B|300:180,1,80,0|2
332,324,13625,1,8
180,324,13875,1,0
256,240,14125,5,4
256,240,14250,1,2
324,112,14500,1,0
324,112,14625,1,2
192,56,14875,1,4
192,56,15000,1,2
256,164,15250,1,0
256,164,15375,1,2
256,20,15625,1,8
120,56,15875,1,0
256,92,16125,1,6
20,152,18375,5,0
180,136,18625,1,8
52,228,18875,1,0
120,84,19125,1,2
128,244,19375,1,0
48,84,19625,1,8
192,212,19875,1,0
300,72,20125,2,4,B|396:36|444:84|396:144|352:184|372:224|416:260|532:224|528:164,1,320,4|0
472,40,20875,2,2,B|376:72|304:164|272:260|280:320,1,320
404,352,21625,1,8
432,196,21875,1,0
432,196,22000,1,0
432,196,22125,1,4
296,100,22375,5,0
168,196,22625,2,0,B|32:296,1,160,8|0
268,212,23125,2,0,B|168:76,1,160,2|8
252,312,23625,2,0,B|388:212,1,160,8|0
484,96,24125,2,2,B|412:0|320:36|288:120|240:136|200:132|156:116|132:96|80:44,1,400,2|0
72,24,24875,2,2,B|158:66|148:177|67:253|-19:210,1,320
56,108,25625,1,8
176,200,25875,1,0
176,200,26000,1,0
176,200,26125,1,4
316,92,26375,5,0
464,164,26625,2,0,B|394:224|412:336,1,160,2|0
232,316,27125,2,0,B|306:256|284:144,1,160,2|8
136,88,27625,1,8
60,224,27875,1,0
212,132,28125,6,0,B|256:32,1,80,4|2
340,228,28500,2,0,B|384:128,1,80,0|2
256,284,28875,2,0,B|212:184,1,80,4|2
128,380,29250,2,0,B|84:280,1,80,0|2
238,383,29625,2,0,B|406:379,1,160,8|0
512,267,30125,5,4
512,267,30250,1,2
416,152,30500,1,0
416,152,30625,1,2
300,264,30875,1,4
300,264,31000,1,2
236,100,31250,1,0
236,100,31375,1,2
152,256,31625,1,8
300,160,31875,1,0
256,332,32125,1,6
52,52,34625,5,0
152,164,34875,1,0
256,56,35125,1,4
256,56,35625,1,2
256,56,36125,2,4,B|331:63|364:136|320:224,1,160,4|0
320,312,36625,1,8
204,228,36875,1,0
104,328,37125,2,2,B|24:287|44:188,1,160
92,60,37625,1,8
212,148,37875,1,0
268,104,38000,1,0
324,60,38125,2,0,B|452:184,1,160,4|0
504,300,38625,1,8
364,340,38875,1,0
232,280,39125,6,2,B|150:282|69:198|105:87|179:53,2,320,2|2|6
280,148,40375,1,0
400,228,40625,2,0,B|520:368,1,160,8|0
480,192,41125,1,2
324,220,41375,1,2
168,256,41625,1,8
72,148,41875,1,2
48,84,42000,1,2
96,36,42125,2,0,B|164:108|256:44,1,160,6|0
400,72,42625,1,2
440,236,42875,1,2
464,300,43000,1,2
416,348,43125,2,0,B|348:276|256:340,1,160,6|0
112,312,43625,1,2
140,188,43875,1,0
52,64,44125,5,6
208,48,44375,1,0
344,132,44625,1,8
448,256,44875,2,2,B|401:321|285:337|217:242|233:163,2,320,2|2|0
326,211,46125,2,2,B|279:146|163:130|95:225|111:304,1,320,6|0
230,287,46875,2,2,B|277:352|393:368|461:273|445:194,1,320,6|8
376,80,47625,1,8
376,80,48125,6,0,B|304:128|216:96,1,160,4|0
84,56,48625,1,8
152,200,48875,1,0
44,320,49125,2,0,B|121:364|204:320,1,160,4|0
336,240,49625,5,8
256,148,49875,1,0
176,240,50125,1,0
340,144,50625,1,0
420,236,50875,1,0
500,144,51125,1,2
172,144,51625,1,2
92,236,51875,1,0
12,144,52125,6,0,B|160:48,1,160,4|0
304,76,52625,1,8
256,228,52875,1,0
216,112,53125,2,0,B|364:208,1,160,2|0
508,180,53625,1,8
460,28,53875,1,0
344,96,54125,1,2
228,8,54375,1,0
153,116,54625,1,2
72,220,54875,1,0
180,295,55125,1,2
284,376,55375,1,0
359,268,55625,1,2
440,164,55875,1,0
352,160,56125,6,0,B|466:294,1,160,4|0
312,228,56625,1,8
200,300,56875,1,0
160,160,57125,2,0,B|46:294,1,160,4|0
200,228,57625,1,8
312,300,57875,1,0
444,208,58125,2,0,B|362:164|380:56,1,160,2|0
344,12,58500,1,0
272,4,58625,2,0,B|232:88|120:68,1,160,2|0
68,176,59125,2,0,B|148:220|132:328,1,160,2|0
168,372,59500,1,0
240,380,59625,2,0,B|280:296|392:316,1,160,2|0
456,176,60125,5,6
328,80,60375,1,0
216,196,60625,1,8
72,136,60875,2,2,B|54:209|91:305|191:336|269:306,2,320,2|2|0
200,224,62125,2,2,B|182:150|219:54|319:23|397:53,1,320,2|0
480,179,62875,2,2,B|499:252|462:348|362:379|284:349,1,320,2|0
136,296,63625,2,0,B|67:220|140:136,1,160,8|0
256,56,64125,5,6
284,212,64375,1,0
440,180,64625,1,8
420,24,64875,1,0
300,132,65125,1,6
272,288,65375,1,0
116,256,65625,1,8
136,100,65875,1,0
256,8,66125,1,4
256,56,68125,6,0,B|298:128|244:237|123:241|74:173,1,320
132,80,68875,2,2,B|344:328,1,320
456,224,69625,1,8
340,116,69875,1,0
340,116,70000,1,0
340,116,70125,1,4
228,4,70375,5,0
256,160,70625,2,0,B|186:224|88:168,1,160,2|0
148,332,71125,2,0,B|216:396|316:340,1,160,2|8
424,248,71625,1,8
336,112,71875,1,0
336,112,72000,1,0
336,112,72125,1,4
228,208,72375,2,0,B|139:179|144:80,1,160,0|8
268,56,72875,2,2,B|272:164|220:272|120:308|72:308,1,320
24,192,73625,1,8
92,64,73875,1,0
92,64,74000,1,0
92,64,74125,1,4
224,140,74375,5,0
340,224,74625,2,0,B|412:211|428:121|363:77,1,160,2|0
268,192,75125,2,0,B|196:205|180:295|245:339,1,160,2|0
268,192,75625,2,0,B|104:168,1,160,8|0
24,52,76125,6,0,B|132:40,1,80
176,32,76375,1,2
348,60,76625,1,2
248,164,76875,1,2
264,20,77125,1,2
324,140,77375,1,2
180,116,77625,1,2
240,240,77875,1,0
256,92,78125,1,4
100,124,78375,5,0
8,256,78625,2,0,B|64:332|176:304,1,160,8|0
304,260,79125,2,0,B|248:184|136:212,1,160,2|0
304,260,79625,1,8
460,284,79875,1,2
420,128,80125,6,0,B|332:128,1,80,4|0
256,124,80375,1,2
344,260,80625,1,2
168,260,80875,1,2
384,192,81125,1,2
256,260,81375,1,2
168,124,81625,1,2
344,124,81875,1,2
128,192,82125,1,4
48,192,82250,6,0,B|48:84|152:52,1,160,2|0
204,44,82625,2,0,B|204:152|308:184,1,160,2|0
352,160,83000,2,0,B|244:160|212:264,1,160,2|0
192,316,83375,2,0,B|84:316|52:212,1,160,2|2
32,88,83875,1,2
172,8,84125,1,4
256,192,84250,12,6,86125
256,192,86250,12,4,87125
256,100,88125,6,2,B|308:116|368:104|404:16,1,160,6|0
256,100,88625,1,8
136,180,88875,1,0
8,96,89125,2,0,B|-28:168|16:232|68:256,1,160,2|0
164,312,89625,1,8
288,236,89875,1,2
288,236,90000,1,2
288,236,90125,2,2,B|452:164,1,160,6|0
476,32,90625,1,8
332,104,90875,1,0
180,104,91125,5,6
36,32,91375,1,8
56,164,91625,1,8
56,164,92125,2,0,B|260:208,1,160,6|0
84,296,92625,1,8
220,376,92875,1,0
320,268,93125,2,0,B|524:224,1,160,6|0
432,80,93625,1,8
296,152,93875,1,2
296,152,94000,1,2
296,152,94125,2,2,B|232:164|176:132|164:52,1,160,6|0
216,232,94625,2,2,B|280:220|336:252|348:332,1,160,2|0
341,304,95000,1,0
341,304,95125,2,0,B|369:84,1,160,2|0
171,80,95625,2,0,B|143:300,1,160,2|0
43,358,96125,5,6
81,219,96375,1,0
169,332,96625,1,8
304,272,96875,2,2,B|388:252|426:161|418:63|344:19,2,320,2|2|0
240,144,98125,2,2,B|219:244|50:229|65:60|168:58,1,320
240,144,98875,2,2,B|260:43|429:58|414:227|311:229,1,320,2|0
180,292,99625,2,0,B|80:304|36:208,1,160,2|0
48,64,100125,6,0,B|224:112,1,160,4|0
348,52,100625,2,0,B|524:4,1,160,2|0
504,172,101125,2,0,B|328:124,1,160,2|0
204,184,101625,2,0,B|28:232,1,160,2|0
49,226,102000,1,0
49,226,102125,1,2
256,324,102625,5,8
384,256,102875,1,0
256,188,103125,1,6
256,188,103625,1,2
128,256,103875,1,0
256,324,104125,6,0,B|324:252|432:316,1,160,6|0
492,168,104625,1,8
332,188,104875,1,0
256,60,105125,2,0,B|188:132|80:68,1,160,6|0
20,216,105625,1,8
180,196,105875,1,0
368,156,106125,2,0,B|418:184|462:234|408:296,1,160,2|0
220,80,106625,2,0,B|248:30|298:-14|360:40,1,160,2|0
144,228,107125,2,0,B|94:200|50:150|104:88,1,160,2|0
292,304,107625,2,0,B|264:354|214:398|152:344,1,160,2|0
44,216,108125,6,0,B|145:221|172:132,1,160,6|0
304,224,108625,1,8
408,104,108875,1,0
468,216,109125,2,0,B|367:221|340:132,1,160,6|0
208,224,109625,1,8
104,104,109875,1,0
256,56,110125,2,0,B|144:180,1,160,2|0
256,328,110625,2,0,B|368:204,1,160,2|0
208,244,111125,2,0,B|96:368,1,160,2|0
304,140,111625,2,0,B|416:16,1,160,2|0
252,20,112125,5,6
112,60,112375,1,0
72,200,112625,1,8
158,316,112875,2,2,B|236:321|324:259|326:152|278:89,2,320,2|2|0
176,168,114125,2,2,B|214:236|313:276|405:220|431:145,1,320,2|0
328,64,114875,2,2,B|259:102|219:201|275:293|350:319,1,320,2|0
488,340,115625,2,0,B|456:172,1,160,2|0
416,72,116125,5,6
288,140,116375,1,0
164,68,116625,1,8
36,136,116875,1,0
104,264,117125,1,6
232,332,117375,1,0
356,260,117625,1,8
484,328,117875,1,0
356,384,118125,1,6
256,12,128125,5,4
256,12,128250,1,2
336,128,128500,1,0
336,128,128625,1,2
400,0,128875,1,0
400,0,129000,1,2
492,112,129250,1,0
492,112,129375,1,2
440,248,129625,2,2,B|272:284,1,160
256,108,130125,5,4
256,108,130250,1,2
176,224,130500,1,0
176,224,130625,1,2
112,96,130875,1,0
112,96,131000,1,2
20,208,131250,1,0
20,208,131375,1,2
72,344,131625,2,2,B|240:380,1,160
408,376,132125,6,0,B|512:352|584:248|592:-32|416:-48|256:-80|96:-16|56:88|8:224|88:304|144:336|184:368|256:368|256:368|328:368|368:336|424:304|504:224|456:88|416:-16|256:-80|96:-48|-80:-32|-72:248|0:352|104:376,1,2240,6|0
256,192,135875,5,2
256,192,136000,1,0
256,192,136125,1,4
136,104,136375,1,0
132,240,136625,1,8
133,240,136750,1,0
256,280,137000,1,0
255,280,137125,1,8
256,280,137250,1,0
256,280,137375,1,0
380,240,137625,1,8
376,104,137875,1,0
256,124,138125,5,4
256,124,138375,1,0
144,192,138625,1,8
144,192,138750,1,0
256,260,139000,1,0
256,260,139125,1,8
256,260,139250,1,0
256,260,139375,1,0
368,192,139625,1,8
256,124,139875,1,0
256,124,140000,1,0
256,124,140125,2,2,B|188:112|212:76|188:36|256:20,1,160,6|2
332,128,140625,5,8
332,128,140750,1,0
332,256,141000,1,0
332,256,141125,1,8
332,256,141250,1,0
332,256,141375,1,0
180,256,141625,1,8
180,128,141875,1,0
256,56,142125,5,4
256,56,142375,1,0
256,160,142625,1,8
256,160,142750,1,0
256,264,143000,1,0
256,264,143125,1,8
256,264,143250,1,0
256,264,143375,1,0
188,352,143625,1,8
324,352,143875,1,0
324,352,144000,1,0
324,352,144125,2,0,B|492:352,1,160,6|2
392,280,144625,5,8
392,280,144750,1,0
324,192,145000,1,0
324,192,145125,1,8
324,192,145250,1,0
324,192,145375,1,0
188,192,145625,1,8
120,280,145875,1,0
256,288,146125,5,4
256,288,146375,1,0
256,176,146625,1,8
256,176,146750,1,0
176,96,147000,1,0
176,96,147125,1,8
176,96,147250,1,0
176,96,147375,1,0
256,16,147625,1,8
336,96,147875,1,0
336,96,148000,1,0
336,96,148125,2,6,B|400:156|388:224|364:248,1,160,6|2
256,272,148625,5,8
240,264,148750,1,0
240,180,149000,1,0
256,172,149125,1,8
272,164,149250,1,0
288,156,149375,1,0
256,64,149625,1,8
256,64,149875,1,0
116,180,150125,5,0
120,200,150250,1,0
132,224,150375,1,0
152,236,150500,1,0
176,240,150625,1,8
208,240,150750,1,0
232,236,150875,1,0
248,216,151000,1,0
256,192,151125,1,8
260,168,151250,1,0
272,144,151375,1,8
292,132,151500,1,0
316,128,151625,1,8
348,128,151750,1,8
372,132,151875,1,8
388,152,152000,1,0
404,184,152125,6,0,B|436:250|377:334|292:300,1,160,6|0
108,200,152625,2,0,B|76:134|135:50|220:84,1,160,6|0
256,192,153125,2,0,B|256:100,1,80,2|0
256,192,153375,2,0,B|256:368,2,160,2|8|0
360,60,154125,5,0
360,60,154250,1,0
360,60,154375,1,2
256,12,154625,1,0
256,12,154750,1,0
256,12,154875,1,2
154,64,155125,1,0
154,64,155250,1,2
155,63,155375,2,0,B|87:119|115:191|179:211|227:179,2,160,0|8|0
163,74,156000,5,0
163,74,156125,1,0
163,74,156250,2,2,B|174:151|299:265|445:180|473:106,1,400,2|0
320,80,157125,2,2,B|224:88|184:188|224:288|320:295,1,320
348,292,157750,1,0
380,280,157875,1,0
404,260,158000,1,0
412,236,158125,1,0
412,208,158250,1,0
404,180,158375,1,0
264,68,158625,2,0,B|184:104,2,80,2|0|2
164,216,159125,2,0,B|244:180,2,80,2|0|2
56,144,159625,5,8
64,276,159875,1,8
64,276,160000,1,8
64,276,160125,2,0,B|24:352,2,80,2|0|0
128,288,160500,2,0,B|136:188,2,80,2|0|0
192,300,160875,2,0,B|200:400,2,80,2|0|0
240,256,161250,2,0,B|304:176,2,80,2|0|0
284,304,161625,2,0,B|356:380,2,80,2|0|0
328,256,162000,6,0,B|456:236,2,80,0|2|0
308,192,162375,2,0,B|180:172,2,80,0|2|0
340,136,162750,2,0,B|468:116,2,80,0|2|0
284,100,163125,2,0,B|264:-28,2,80,0|2|0
224,128,163500,2,0,B|204:256,2,80,0|2|0
180,76,163875,6,0,B|92:52,2,80,2|0|0
144,132,164250,2,0,B|72:184,2,80,2|0|0
168,196,164625,2,0,B|240:248,2,80,2|0|0
136,256,165000,2,0,B|96:340,2,80,2|0|0
188,296,165375,2,0,B|228:380,2,80,2|0|0
236,252,165750,1,0
236,252,165875,1,2
364,276,166125,6,2,B|408:176|360:156|320:168|296:176|268:132|264:112|272:76|304:52|328:40,1,240,2|0
264,24,166625,2,2,B|308:124|260:144|220:132|196:124|168:168|164:188|172:224|204:248|228:260,1,240,2|0
192,280,167125,1,0
320,376,167375,1,0
192,376,167625,1,0
256,328,167750,1,0
320,280,167875,1,0
256,124,168125,1,6
256,192,168250,12,0,170125
256,192,171125,12,6,172125
48,56,172375,5,0
20,184,172625,2,0,B|16:264|92:316|152:304,1,160,8|0
240,300,173125,1,2
200,176,173375,1,0
324,220,173625,2,0,B|360:220|416:258|412:338,1,160,8|0
412,334,174000,1,0
412,334,174125,2,0,B|456:156,1,160,6|0
398,35,174625,2,0,B|220:-8,1,160,2|0
245,0,175000,1,0
245,0,175125,2,0,B|201:178,1,160,6|0
259,299,175625,2,0,B|437:342,1,160,2|0
424,176,176125,5,6
272,128,176375,1,0
116,152,176625,1,8
173,253,176875,2,2,B|257:233|295:142|287:44|213:0,2,320,2|2|0
28,204,178125,2,2,B|356:316,1,320
172,360,178875,2,2,B|500:248,1,320,2|0
384,148,179625,2,0,B|292:168|224:96|232:44,1,160,2|0
244,93,180000,1,0
244,93,180125,6,0,B|64:120,1,160,6|0
100,268,180625,2,0,B|256:296,1,160,8|0
257,296,181000,1,0
256,296,181125,2,0,B|413:267,1,160,6|0
426,116,181625,2,0,B|267:93,1,160,8|2
267,93,182000,5,2
267,93,182125,2,2,B|180:112|168:212,1,160,2|0
140,380,182625,2,0,B|227:361|239:261,1,160,8|0
62,169,183125,2,2,B|80:256|180:268,1,160,2|0
348,296,183625,2,0,B|329:208|229:196,1,160,8|0
64,172,184125,1,6
256,192,184250,12,2,185625
48,188,186125,6,2,B|96:108|256:108|256:192|256:276|416:276|464:196,1,480,2|0
328,144,187125,2,0,B|296:316,1,160,2|0
184,240,187625,2,0,B|216:68,1,160,2|0
256,192,188125,1,6
256,192,188250,12,2,189625
464,188,190125,6,2,B|416:108|256:108|256:192|256:276|96:276|48:196,1,480,2|0
184,144,191125,2,0,B|216:316,1,160,2|0
328,240,191625,2,0,B|296:68,1,160,2|0
164,32,192125,5,6
28,84,192375,1,0
28,228,192625,1,8
128,332,192875,2,2,B|160:224|300:172|408:244,2,320,2|2|0
276,356,194125,2,2,B|384:324|436:184|364:76,1,320
236,28,194875,2,2,B|128:60|76:200|148:308,1,320,2|0
280,268,195625,2,0,B|232:116,1,160,2|0
104,52,196125,5,6
136,192,196375,1,0
116,344,196625,1,8
256,312,196875,1,0
332,312,197000,1,0
408,332,197125,1,6
392,264,197250,1,0
376,192,197375,1,0
396,40,197625,1,8
256,72,197875,5,0
256,72,198000,1,0
256,72,198125,1,6
136,192,198625,1,6
256,312,199125,1,6
376,192,199625,1,6
256,192,200125,1,6

View File

@ -15,6 +15,7 @@ using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Catch.Skinning.Argon;
@ -235,5 +236,17 @@ namespace osu.Game.Rulesets.Catch
}),
};
}
/// <seealso cref="CatchHitObject.ApplyDefaultsToSelf"/>
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
{
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
preempt /= rate;
adjustedDifficulty.ApproachRate = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
return adjustedDifficulty;
}
}
}

View File

@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN);
Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize);
}
@ -189,6 +189,21 @@ namespace osu.Game.Rulesets.Catch.Objects
// The half of the height of the osu! playfield.
public const float DEFAULT_LEGACY_CONVERT_Y = 192;
/// <summary>
/// Minimum preempt time at AR=10.
/// </summary>
public const double PREEMPT_MIN = 450;
/// <summary>
/// Median preempt time at AR=5.
/// </summary>
public const double PREEMPT_MID = 1200;
/// <summary>
/// Maximum preempt time at AR=0.
/// </summary>
public const double PREEMPT_MAX = 1800;
/// <summary>
/// The Y position of the hit object is not used in the normal osu!catch gameplay.
/// It is preserved to maximize the backward compatibility with the legacy editor, in which the mappers use the Y position to organize the patterns.

View File

@ -10,7 +10,6 @@ using osu.Framework.Bindables;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@ -103,8 +102,7 @@ namespace osu.Game.Rulesets.Catch.Objects
AddNested(new TinyDroplet
{
StartTime = t + lastEvent.Value.Time,
X = ClampToPlayfield(EffectiveX + Path.PositionAt(
lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X),
X = EffectiveX + Path.PositionAt(lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X,
});
}
}
@ -121,7 +119,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Samples = dropletSamples,
StartTime = e.Time,
X = ClampToPlayfield(EffectiveX + Path.PositionAt(e.PathProgress).X),
X = EffectiveX + Path.PositionAt(e.PathProgress).X,
});
break;
@ -132,16 +130,14 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Samples = this.GetNodeSamples(nodeIndex++),
StartTime = e.Time,
X = ClampToPlayfield(EffectiveX + Path.PositionAt(e.PathProgress).X),
X = EffectiveX + Path.PositionAt(e.PathProgress).X,
});
break;
}
}
}
public float EndX => ClampToPlayfield(EffectiveX + this.CurvePositionAt(1).X);
public float ClampToPlayfield(float value) => Math.Clamp(value, 0, CatchPlayfield.WIDTH);
public float EndX => EffectiveX + this.CurvePositionAt(1).X;
[JsonIgnore]
public double Duration

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
@ -100,16 +99,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
return SkinUtils.As<TValue>(new Bindable<float>(30));
case LegacyManiaSkinConfigurationLookups.ColumnWidth:
float width;
bool isSpecialColumn = stage.IsSpecialColumn(columnIndex);
// Best effort until we have better mobile support.
if (RuntimeInfo.IsMobile)
width = 170 * Math.Min(1, 7f / beatmap.TotalColumns) * (isSpecialColumn ? 1.8f : 1);
else
width = 60 * (isSpecialColumn ? 2 : 1);
float width = 60 * (isSpecialColumn ? 2 : 1);
return SkinUtils.As<TValue>(new Bindable<float>(width));

View File

@ -34,7 +34,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
FallbackColumnIndex = "S";
else
{
int distanceToEdge = Math.Min(Column.Index, (stage.Columns - 1) - Column.Index);
// Account for cases like dual-stage (assume that all stages have the same column count for now).
int columnInStage = Column.Index % stage.Columns;
int distanceToEdge = Math.Min(columnInStage, (stage.Columns - 1) - columnInStage);
FallbackColumnIndex = distanceToEdge % 2 == 0 ? "1" : "2";
}
}

View File

@ -3,14 +3,17 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
@ -60,6 +63,12 @@ namespace osu.Game.Rulesets.Mania.UI
onSkinChanged();
}
protected override void LoadComplete()
{
base.LoadComplete();
updateMobileSizing();
}
private void onSkinChanged()
{
for (int i = 0; i < stageDefinition.Columns; i++)
@ -77,12 +86,15 @@ namespace osu.Game.Rulesets.Mania.UI
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i))
?.Value;
if (width == null)
// only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration)
columns[i].Width = stageDefinition.IsSpecialColumn(i) ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH;
else
columns[i].Width = width.Value;
bool isSpecialColumn = stageDefinition.IsSpecialColumn(i);
// only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration)
width ??= isSpecialColumn ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH;
columns[i].Width = width.Value;
}
updateMobileSizing();
}
/// <summary>
@ -92,10 +104,29 @@ namespace osu.Game.Rulesets.Mania.UI
/// <param name="content">The content.</param>
public void SetContentForColumn(int column, TContent content) => columns[column].Child = content;
public new MarginPadding Padding
private void updateMobileSizing()
{
get => base.Padding;
set => base.Padding = value;
if (!IsLoaded || !RuntimeInfo.IsMobile)
return;
// GridContainer+CellContainer containing this stage (gets split up for dual stages).
Vector2? containingCell = this.FindClosestParent<Stage>()?.Parent?.DrawSize;
// Will be null in tests.
if (containingCell == null)
return;
float aspectRatio = containingCell.Value.X / containingCell.Value.Y;
// 2.83 is a mostly arbitrary scale-up (170 / 60, based on original implementation for argon)
float mobileAdjust = 2.83f * Math.Min(1, 7f / stageDefinition.Columns);
// 1.92 is a "reference" mobile screen aspect ratio for phones.
// We should scale it back for cases like tablets which aren't so extreme.
mobileAdjust *= aspectRatio / 1.92f;
// Best effort until we have better mobile support.
for (int i = 0; i < stageDefinition.Columns; i++)
columns[i].Width *= mobileAdjust;
}
protected override void Dispose(bool isDisposing)

View File

@ -0,0 +1,65 @@
// 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.Beatmaps;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class OsuRateAdjustedDisplayDifficultyTest
{
private static IEnumerable<float> difficultyValuesToTest()
{
for (float i = 0; i <= 10; i += 0.5f)
yield return i;
}
[TestCaseSource(nameof(difficultyValuesToTest))]
public void TestApproachRateIsUnchangedWithRateEqualToOne(float originalApproachRate)
{
var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
}
[TestCaseSource(nameof(difficultyValuesToTest))]
public void TestOverallDifficultyIsUnchangedWithRateEqualToOne(float originalOverallDifficulty)
{
var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty));
}
[Test]
public void TestRateBelowOne()
{
var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(2.22).Within(0.01));
}
[Test]
public void TestRateAboveOne()
{
var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(7.77).Within(0.01));
}
}
}

View File

@ -37,6 +37,16 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary>
public const double PREEMPT_MIN = 450;
/// <summary>
/// Median preempt time at AR=5.
/// </summary>
public const double PREEMPT_MID = 1200;
/// <summary>
/// Maximum preempt time at AR=0.
/// </summary>
public const double PREEMPT_MAX = 1800;
public double TimePreempt = 600;
public double TimeFadeIn = 400;
@ -148,7 +158,7 @@ namespace osu.Game.Rulesets.Osu.Objects
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN);
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN);
// Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR.
// This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above.

View File

@ -331,5 +331,23 @@ namespace osu.Game.Rulesets.Osu
}
public override RulesetSetupSection CreateEditorSetupSection() => new OsuSetupSection();
/// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/>
/// <seealso cref="OsuHitWindows"/>
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
{
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
preempt /= rate;
adjustedDifficulty.ApproachRate = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
var greatHitWindowRange = OsuHitWindows.OSU_RANGES.Single(range => range.Result == HitResult.Great);
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
greatHitWindow /= rate;
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
return adjustedDifficulty;
}
}
}

View File

@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
/// </summary>
public const double MISS_WINDOW = 400;
private static readonly DifficultyRange[] osu_ranges =
internal static readonly DifficultyRange[] OSU_RANGES =
{
new DifficultyRange(HitResult.Great, 80, 50, 20),
new DifficultyRange(HitResult.Ok, 140, 100, 60),
@ -34,6 +34,6 @@ namespace osu.Game.Rulesets.Osu.Scoring
return false;
}
protected override DifficultyRange[] GetRanges() => osu_ranges;
protected override DifficultyRange[] GetRanges() => OSU_RANGES;
}
}

View File

@ -0,0 +1,52 @@
// 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.Beatmaps;
namespace osu.Game.Rulesets.Taiko.Tests
{
[TestFixture]
public class TaikoRateAdjustedDisplayDifficultyTest
{
private static IEnumerable<float> difficultyValuesToTest()
{
for (float i = 0; i <= 10; i += 0.5f)
yield return i;
}
[TestCaseSource(nameof(difficultyValuesToTest))]
public void TestOverallDifficultyIsUnchangedWithRateEqualToOne(float originalOverallDifficulty)
{
var ruleset = new TaikoRuleset();
var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty));
}
[Test]
public void TestRateBelowOne()
{
var ruleset = new TaikoRuleset();
var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(1.11).Within(0.01));
}
[Test]
public void TestRateAboveOne()
{
var ruleset = new TaikoRuleset();
var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(8.89).Within(0.01));
}
}
}

View File

@ -7,7 +7,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring
{
public class TaikoHitWindows : HitWindows
{
private static readonly DifficultyRange[] taiko_ranges =
internal static readonly DifficultyRange[] TAIKO_RANGES =
{
new DifficultyRange(HitResult.Great, 50, 35, 20),
new DifficultyRange(HitResult.Ok, 120, 80, 50),
@ -27,6 +27,6 @@ namespace osu.Game.Rulesets.Taiko.Scoring
return false;
}
protected override DifficultyRange[] GetRanges() => taiko_ranges;
protected override DifficultyRange[] GetRanges() => TAIKO_RANGES;
}
}

View File

@ -264,5 +264,18 @@ namespace osu.Game.Rulesets.Taiko
}), true)
};
}
/// <seealso cref="TaikoHitWindows"/>
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
{
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
var greatHitWindowRange = TaikoHitWindows.TAIKO_RANGES.Single(range => range.Result == HitResult.Great);
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
greatHitWindow /= rate;
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
return adjustedDifficulty;
}
}
}

View File

@ -283,15 +283,17 @@ namespace osu.Game.Tests.Visual.Navigation
openSkinEditor();
}
[Test]
public void TestOpenSkinEditorGameplaySceneWhenDifferentRulesetActive()
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
public void TestOpenSkinEditorGameplaySceneWhenDifferentRulesetActive(int rulesetId)
{
BeatmapSetInfo beatmapSet = null!;
AddStep("import beatmap", () => beatmapSet = BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely());
AddStep("select mania difficulty", () =>
AddStep($"select difficulty for ruleset w/ ID {rulesetId}", () =>
{
var beatmap = beatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 3);
var beatmap = beatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == rulesetId);
Game.Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(beatmap);
});

View File

@ -220,7 +220,7 @@ namespace osu.Game.Tests.Visual.Online
public void TestSelectedModsDontAffectStatistics()
{
AddStep("show map", () => overlay.ShowBeatmapSet(getBeatmapSet()));
AddAssert("AR displayed as 0", () => overlay.ChildrenOfType<AdvancedStats.StatisticRow>().Single(s => s.Title == BeatmapsetsStrings.ShowStatsAr).Value == (0, null));
AddAssert("AR displayed as 0", () => overlay.ChildrenOfType<AdvancedStats.StatisticRow>().Single(s => s.Title == BeatmapsetsStrings.ShowStatsAr).Value, () => Is.EqualTo((0, 0)));
AddStep("set AR10 diff adjust", () => SelectedMods.Value = new[]
{
new OsuModDifficultyAdjust
@ -228,7 +228,7 @@ namespace osu.Game.Tests.Visual.Online
ApproachRate = { Value = 10 }
}
});
AddAssert("AR still displayed as 0", () => overlay.ChildrenOfType<AdvancedStats.StatisticRow>().Single(s => s.Title == BeatmapsetsStrings.ShowStatsAr).Value == (0, null));
AddAssert("AR still displayed as 0", () => overlay.ChildrenOfType<AdvancedStats.StatisticRow>().Single(s => s.Title == BeatmapsetsStrings.ShowStatsAr).Value, () => Is.EqualTo((0, 0)));
}
[Test]

View File

@ -192,7 +192,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select collection", () =>
{
InputManager.MoveMouseTo(getCollectionDropdownItems().ElementAt(1));
InputManager.MoveMouseTo(getCollectionDropdownItemAt(1));
InputManager.Click(MouseButton.Left);
});
@ -206,7 +206,8 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("click manage collections filter", () =>
{
InputManager.MoveMouseTo(getCollectionDropdownItems().Last());
int lastItemIndex = control.ChildrenOfType<CollectionDropdown>().Single().Items.Count() - 1;
InputManager.MoveMouseTo(getCollectionDropdownItemAt(lastItemIndex));
InputManager.Click(MouseButton.Left);
});
@ -232,10 +233,10 @@ namespace osu.Game.Tests.Visual.SongSelect
private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) =>
AddUntilStep($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'",
// A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872
() => shouldContain == (getCollectionDropdownItems().Any(i => i.ChildrenOfType<CompositeDrawable>().OfType<IHasText>().First().Text == collectionName)));
() => shouldContain == control.ChildrenOfType<Menu.DrawableMenuItem>().Any(i => i.ChildrenOfType<CompositeDrawable>().OfType<IHasText>().First().Text == collectionName));
private IconButton getAddOrRemoveButton(int index)
=> getCollectionDropdownItems().ElementAt(index).ChildrenOfType<IconButton>().Single();
=> getCollectionDropdownItemAt(index).ChildrenOfType<IconButton>().Single();
private void addExpandHeaderStep() => AddStep("expand header", () =>
{
@ -249,7 +250,11 @@ namespace osu.Game.Tests.Visual.SongSelect
InputManager.Click(MouseButton.Left);
});
private IEnumerable<Dropdown<CollectionFilterMenuItem>.DropdownMenu.DrawableDropdownMenuItem> getCollectionDropdownItems()
=> control.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Dropdown<CollectionFilterMenuItem>.DropdownMenu.DrawableDropdownMenuItem>();
private Menu.DrawableMenuItem getCollectionDropdownItemAt(int index)
{
// todo: we should be able to use Items, but apparently that's not guaranteed to be ordered... see: https://github.com/ppy/osu-framework/pull/6079
CollectionFilterMenuItem item = control.ChildrenOfType<CollectionDropdown>().Single().ItemSource.ElementAt(index);
return control.ChildrenOfType<Menu.DrawableMenuItem>().Single(i => i.Item.Text.Value == item.CollectionName);
}
}
}

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
@ -38,6 +39,9 @@ namespace osu.Game.Tests.Visual.UserInterface
private TestModSelectOverlay modSelectOverlay = null!;
[Resolved]
private OsuConfigManager configManager { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
@ -566,17 +570,33 @@ namespace osu.Game.Tests.Visual.UserInterface
}
[Test]
public void TestSearchFocusChangeViaKey()
public void TestTextSearchActiveByDefault()
{
configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true);
createScreen();
const Key focus_switch_key = Key.Tab;
AddUntilStep("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);
AddStep("press tab", () => InputManager.Key(focus_switch_key));
AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus);
AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search text box unfocused", () => !modSelectOverlay.SearchTextBox.HasFocus);
AddStep("press tab", () => InputManager.Key(focus_switch_key));
AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus);
AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);
}
[Test]
public void TestTextSearchNotActiveByDefault()
{
configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, false);
createScreen();
AddUntilStep("search text box not focused", () => !modSelectOverlay.SearchTextBox.HasFocus);
AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);
AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search text box unfocused", () => !modSelectOverlay.SearchTextBox.HasFocus);
}
[Test]
@ -787,7 +807,8 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.MoveMouseTo(this.ChildrenOfType<ModPresetPanel>().Single(preset => preset.Preset.Value.Name == "Half Time 0.5x"));
InputManager.Click(MouseButton.Left);
});
AddAssert("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.5));
AddAssert("difficulty multiplier display shows correct value",
() => modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.1).Within(Precision.DOUBLE_EPSILON));
// this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation,
// it is instrumental in the reproduction of the failure scenario that this test is supposed to cover.
@ -796,7 +817,8 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick());
AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType<ModSettingsArea>().Single()
.ChildrenOfType<RevertToDefaultButton<double>>().Single().TriggerClick());
AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.7));
AddUntilStep("difficulty multiplier display shows correct value",
() => modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON));
}
private void waitForColumnLoad() => AddUntilStep("all column content loaded", () =>

View File

@ -21,6 +21,10 @@ namespace osu.Game.Tournament.Models
public double StarRating { get; set; }
public int EndTimeObjectCount { get; set; }
public int TotalObjectCount { get; set; }
public IBeatmapMetadataInfo Metadata { get; set; } = new BeatmapMetadata();
public IBeatmapDifficultyInfo Difficulty { get; set; } = new BeatmapDifficulty();
@ -41,6 +45,8 @@ namespace osu.Game.Tournament.Models
Metadata = beatmap.Metadata;
Difficulty = beatmap.Difficulty;
Covers = beatmap.BeatmapSet?.Covers ?? new BeatmapSetOnlineCovers();
EndTimeObjectCount = beatmap.EndTimeObjectCount;
TotalObjectCount = beatmap.TotalObjectCount;
}
public bool Equals(IBeatmapInfo? other) => other is TournamentBeatmap b && this.MatchesOnlineID(b);

View File

@ -20,7 +20,6 @@ using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Screens.Play;
using Realms;
namespace osu.Game
{
@ -177,9 +176,13 @@ namespace osu.Game
{
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)));
HashSet<Guid> beatmapIds = new HashSet<Guid>();
realmAccess.Run(r =>
{
foreach (var b in r.All<BeatmapInfo>().Where(b => b.TotalObjectCount == 0))
beatmapIds.Add(b.ID);
});
Logger.Log($"Found {beatmapIds.Count} beatmaps which require reprocessing.");

View File

@ -21,9 +21,6 @@ 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()
{
}
@ -47,9 +44,6 @@ namespace osu.Game.Beatmaps
difficulty.SliderMultiplier = SliderMultiplier;
difficulty.SliderTickRate = SliderTickRate;
difficulty.EndTimeObjectCount = EndTimeObjectCount;
difficulty.TotalObjectCount = TotalObjectCount;
}
public virtual void CopyFrom(IBeatmapDifficultyInfo other)
@ -61,9 +55,6 @@ namespace osu.Game.Beatmaps
SliderMultiplier = other.SliderMultiplier;
SliderTickRate = other.SliderTickRate;
EndTimeObjectCount = other.EndTimeObjectCount;
TotalObjectCount = other.TotalObjectCount;
}
}
}

View File

@ -388,9 +388,7 @@ namespace osu.Game.Beatmaps
OverallDifficulty = decodedDifficulty.OverallDifficulty,
ApproachRate = decodedDifficulty.ApproachRate,
SliderMultiplier = decodedDifficulty.SliderMultiplier,
SliderTickRate = decodedDifficulty.SliderTickRate,
EndTimeObjectCount = decoded.HitObjects.Count(h => h is IHasDuration),
TotalObjectCount = decoded.HitObjects.Count
SliderTickRate = decodedDifficulty.SliderTickRate
};
var metadata = new BeatmapMetadata
@ -428,6 +426,8 @@ namespace osu.Game.Beatmaps
GridSize = decodedInfo.GridSize,
TimelineZoom = decodedInfo.TimelineZoom,
MD5Hash = memoryStream.ComputeMD5Hash(),
EndTimeObjectCount = decoded.HitObjects.Count(h => h is IHasDuration),
TotalObjectCount = decoded.HitObjects.Count
};
beatmaps.Add(beatmap);

View File

@ -120,6 +120,10 @@ namespace osu.Game.Beatmaps
[JsonIgnore]
public bool Hidden { get; set; }
public int EndTimeObjectCount { get; set; }
public int TotalObjectCount { get; set; }
/// <summary>
/// Reset any fetched online linking information (and history).
/// </summary>

View File

@ -91,8 +91,8 @@ namespace osu.Game.Beatmaps
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;
beatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration);
beatmapInfo.TotalObjectCount = beatmap.HitObjects.Count;
// And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
workingBeatmapCache.Invalidate(beatmapInfo);

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 System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@ -38,6 +39,8 @@ namespace osu.Game.Beatmaps.Drawables
private readonly Bindable<double> displayedStars = new BindableDouble();
private readonly Container textContainer;
/// <summary>
/// The currently displayed stars of this display wrapped in a bindable.
/// This bindable gets transformed on change rather than instantaneous, if animation is enabled.
@ -116,15 +119,19 @@ namespace osu.Game.Beatmaps.Drawables
Size = new Vector2(8f),
},
Empty(),
starsText = new OsuSpriteText
textContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Bottom = 1.5f },
// todo: this should be size: 12f, but to match up with the design, it needs to be 14.4f
// see https://github.com/ppy/osu-framework/issues/3271.
Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold),
Shadow = false,
AutoSizeAxes = Axes.Y,
Child = starsText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Bottom = 1.5f },
// todo: this should be size: 12f, but to match up with the design, it needs to be 14.4f
// see https://github.com/ppy/osu-framework/issues/3271.
Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold),
Shadow = false,
},
},
}
}
@ -155,6 +162,11 @@ namespace osu.Game.Beatmaps.Drawables
starIcon.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4Extensions.FromHex("303d47");
starsText.Colour = s.NewValue >= 6.5 ? colours.Orange1 : colourProvider?.Background5 ?? Color4.Black.Opacity(0.75f);
// In order to avoid autosize throwing the width of these displays all over the place,
// let's lock in some sane defaults for the text width based on how many digits we're
// displaying.
textContainer.Width = 24 + Math.Max(starsText.Text.ToString().Length - 4, 0) * 6;
}, true);
}
}

View File

@ -1,6 +1,8 @@
// 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;
namespace osu.Game.Beatmaps
{
/// <summary>
@ -44,19 +46,6 @@ 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>
@ -105,5 +94,21 @@ namespace osu.Game.Beatmaps
/// <returns>Value to which the difficulty value maps in the specified range.</returns>
static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range)
=> DifficultyRange(difficulty, range.od0, range.od5, range.od10);
/// <summary>
/// Inverse function to <see cref="DifficultyRange(double,double,double,double)"/>.
/// Maps a value returned by the function above back to the difficulty that produced it.
/// </summary>
/// <param name="difficultyValue">The difficulty-dependent value to be unmapped.</param>
/// <param name="diff0">Minimum of the resulting range which will be achieved by a difficulty value of 0.</param>
/// <param name="diff5">Midpoint of the resulting range which will be achieved by a difficulty value of 5.</param>
/// <param name="diff10">Maximum of the resulting range which will be achieved by a difficulty value of 10.</param>
/// <returns>Value to which the difficulty value maps in the specified range.</returns>
static double InverseDifficultyRange(double difficultyValue, double diff0, double diff5, double diff10)
{
return Math.Sign(difficultyValue - diff5) == Math.Sign(diff10 - diff5)
? (difficultyValue - diff5) / (diff10 - diff5) * 5 + 5
: (difficultyValue - diff5) / (diff5 - diff0) * 5 + 5;
}
}
}

View File

@ -61,5 +61,18 @@ namespace osu.Game.Beatmaps
/// The basic star rating for this beatmap (with no mods applied).
/// </summary>
double StarRating { 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; }
}
}

View File

@ -43,11 +43,14 @@ namespace osu.Game.Collections
private IDisposable? realmSubscription;
private readonly CollectionFilterMenuItem allBeatmapsItem = new AllBeatmapsCollectionFilterMenuItem();
public CollectionDropdown()
{
ItemSource = filters;
Current.Value = new AllBeatmapsCollectionFilterMenuItem();
Current.Value = allBeatmapsItem;
AlwaysShowSearchBar = true;
}
protected override void LoadComplete()
@ -61,37 +64,51 @@ namespace osu.Game.Collections
private void collectionsChanged(IRealmCollection<BeatmapCollection> collections, ChangeSet? changes)
{
var selectedItem = SelectedItem?.Value?.Collection;
var allBeatmaps = new AllBeatmapsCollectionFilterMenuItem();
filters.Clear();
filters.Add(allBeatmaps);
filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c.ToLive(realm))));
if (ShowManageCollectionsItem)
filters.Add(new ManageCollectionsFilterMenuItem());
// This current update and schedule is required to work around dropdown headers not updating text even when the selected item
// changes. It's not great but honestly the whole dropdown menu structure isn't great. This needs to be fixed, but I'll issue
// a warning that it's going to be a frustrating journey.
Current.Value = allBeatmaps;
Schedule(() =>
if (changes == null)
{
// current may have changed before the scheduled call is run.
if (Current.Value != allBeatmaps)
return;
Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection.ID == selectedItem?.ID) ?? filters[0];
});
// Trigger a re-filter if the current item was in the change set.
if (selectedItem != null && changes != null)
filters.Add(allBeatmapsItem);
filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c.ToLive(realm))));
if (ShowManageCollectionsItem)
filters.Add(new ManageCollectionsFilterMenuItem());
}
else
{
foreach (int index in changes.ModifiedIndices)
foreach (int i in changes.DeletedIndices)
filters.RemoveAt(i + 1);
foreach (int i in changes.InsertedIndices)
filters.Insert(i + 1, new CollectionFilterMenuItem(collections[i].ToLive(realm)));
var selectedItem = SelectedItem?.Value;
foreach (int i in changes.NewModifiedIndices)
{
if (collections[index].ID == selectedItem.ID)
var updatedItem = collections[i];
// This is responsible for updating the state of the +/- button and the collection's name.
// TODO: we can probably make the menu items update with changes to avoid this.
filters.RemoveAt(i + 1);
filters.Insert(i + 1, new CollectionFilterMenuItem(updatedItem.ToLive(realm)));
if (updatedItem.ID == selectedItem?.Collection?.ID)
{
// This current update and schedule is required to work around dropdown headers not updating text even when the selected item
// changes. It's not great but honestly the whole dropdown menu structure isn't great. This needs to be fixed, but I'll issue
// a warning that it's going to be a frustrating journey.
Current.Value = allBeatmapsItem;
Schedule(() =>
{
// current may have changed before the scheduled call is run.
if (Current.Value != allBeatmapsItem)
return;
Current.Value = filters.SingleOrDefault(f => f.Collection?.ID == selectedItem.Collection?.ID) ?? filters[0];
});
// Trigger an external re-filter if the current item was in the change set.
RequestFilter?.Invoke();
break;
}
}
}
}

View File

@ -49,6 +49,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.RandomSelectAlgorithm, RandomSelectAlgorithm.RandomPermutation);
SetDefault(OsuSetting.ModSelectHotkeyStyle, ModSelectHotkeyStyle.Sequential);
SetDefault(OsuSetting.ModSelectTextSearchStartsActive, true);
SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f);
@ -416,5 +417,6 @@ namespace osu.Game.Configuration
AutomaticallyDownloadMissingBeatmaps,
EditorShowSpeedChanges,
TouchDisableGameplayTaps,
ModSelectTextSearchStartsActive,
}
}

View File

@ -3,6 +3,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework;
@ -54,6 +57,49 @@ namespace osu.Game.Database
public void UpdateStorage(string stablePath) => cachedStorage = new StableStorage(stablePath, gameHost as DesktopGameHost);
/// <summary>
/// Checks whether a valid location to run a stable import from can be determined starting from the supplied <paramref name="directory"/>.
/// </summary>
/// <param name="directory">The directory to check for stable import eligibility.</param>
/// <param name="stableRoot">
/// If the return value is <see langword="true"/>,
/// this parameter will contain the <see cref="DirectoryInfo"/> to use as the root directory for importing.
/// </param>
public bool IsUsableForStableImport(DirectoryInfo? directory, [NotNullWhen(true)] out DirectoryInfo? stableRoot)
{
if (directory == null)
{
stableRoot = null;
return false;
}
// A full stable installation will have a configuration file present.
// This is the best case scenario, as it may contain a custom beatmap directory we need to traverse to.
if (directory.GetFiles(@"osu!.*.cfg").Any())
{
stableRoot = directory;
return true;
}
// The user may only have their songs or skins folders left.
// We still want to allow them to import based on this.
if (directory.GetDirectories(@"Songs").Any() || directory.GetDirectories(@"Skins").Any())
{
stableRoot = directory;
return true;
}
// The user may have traversed *inside* their songs or skins folders.
if (directory.Parent != null && (directory.Name == @"Songs" || directory.Name == @"Skins"))
{
stableRoot = directory.Parent;
return true;
}
stableRoot = null;
return false;
}
public bool CheckSongsFolderHardLinkAvailability()
{
var stableStorage = GetCurrentStableStorage();

View File

@ -88,9 +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.
/// 38 2023-12-10 Add EndTimeObjectCount and TotalObjectCount to BeatmapInfo.
/// </summary>
private const int schema_version = 37;
private const int schema_version = 38;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.

View File

@ -49,6 +49,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString OpenOsuFolder => new TranslatableString(getKey(@"open_osu_folder"), @"Open osu! folder");
/// <summary>
/// "Export logs"
/// </summary>
public static LocalisableString ExportLogs => new TranslatableString(getKey(@"export_logs"), @"Export logs");
/// <summary>
/// "Change folder location..."
/// </summary>

View File

@ -104,6 +104,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString ModSelectHotkeyStyle => new TranslatableString(getKey(@"mod_select_hotkey_style"), @"Mod select hotkey style");
/// <summary>
/// "Automatically focus search text box in mod select"
/// </summary>
public static LocalisableString ModSelectTextSearchStartsActive => new TranslatableString(getKey(@"mod_select_text_search_starts_active"), @"Automatically focus search text box in mod select");
/// <summary>
/// "no limit"
/// </summary>

View File

@ -41,6 +41,10 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"difficulty_rating")]
public double StarRating { get; set; }
public int EndTimeObjectCount => SliderCount + SpinnerCount;
public int TotalObjectCount => CircleCount + SliderCount + SpinnerCount;
[JsonProperty(@"drain")]
public float DrainRate { get; set; }
@ -108,9 +112,7 @@ namespace osu.Game.Online.API.Requests.Responses
DrainRate = DrainRate,
CircleSize = CircleSize,
ApproachRate = ApproachRate,
OverallDifficulty = OverallDifficulty,
EndTimeObjectCount = SliderCount + SpinnerCount,
TotalObjectCount = CircleCount + SliderCount + SpinnerCount
OverallDifficulty = OverallDifficulty
};
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;

View File

@ -244,6 +244,8 @@ namespace osu.Game.Overlays.FirstRunSetup
[Resolved(canBeNull: true)] // Can't really be null but required to handle potential of disposal before DI completes.
private OsuGameBase? game { get; set; }
private bool changingDirectory;
protected override void LoadComplete()
{
base.LoadComplete();
@ -259,24 +261,37 @@ namespace osu.Game.Overlays.FirstRunSetup
private void onDirectorySelected(ValueChangedEvent<DirectoryInfo?> directory)
{
if (directory.NewValue == null)
{
Current.Value = string.Empty;
if (changingDirectory)
return;
try
{
changingDirectory = true;
if (directory.NewValue == null)
{
Current.Value = string.Empty;
return;
}
// DirectorySelectors can trigger a noop value changed, but `DirectoryInfo` equality doesn't catch this.
if (directory.OldValue?.FullName == directory.NewValue.FullName)
return;
if (legacyImportManager.IsUsableForStableImport(directory.NewValue, out var stableRoot))
{
this.HidePopover();
string path = stableRoot.FullName;
legacyImportManager.UpdateStorage(path);
Current.Value = path;
currentDirectory.Value = stableRoot;
}
}
// DirectorySelectors can trigger a noop value changed, but `DirectoryInfo` equality doesn't catch this.
if (directory.OldValue?.FullName == directory.NewValue.FullName)
return;
if (directory.NewValue?.GetFiles(@"osu!.*.cfg").Any() ?? false)
finally
{
this.HidePopover();
string path = directory.NewValue.FullName;
legacyImportManager.UpdateStorage(path);
Current.Value = path;
changingDirectory = false;
}
}

View File

@ -0,0 +1,142 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Overlays.Mods
{
public partial class AdjustedAttributesTooltip : VisibilityContainer, ITooltip<AdjustedAttributesTooltip.Data?>
{
private FillFlowContainer attributesFillFlow = null!;
private Container content = null!;
private Data? data;
[Resolved]
private OsuColour colours { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
Masking = true;
CornerRadius = 5;
InternalChildren = new Drawable[]
{
content = new Container
{
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray3,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Vertical = 10, Horizontal = 15 },
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new OsuSpriteText
{
Text = "One or more values are being adjusted by mods that change speed.",
},
attributesFillFlow = new FillFlowContainer
{
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both
}
}
}
}
},
};
updateDisplay();
}
private void updateDisplay()
{
attributesFillFlow.Clear();
if (data != null)
{
attemptAdd("CS", bd => bd.CircleSize);
attemptAdd("HP", bd => bd.DrainRate);
attemptAdd("OD", bd => bd.OverallDifficulty);
attemptAdd("AR", bd => bd.ApproachRate);
}
if (attributesFillFlow.Any())
content.Show();
else
content.Hide();
void attemptAdd(string name, Func<BeatmapDifficulty, double> lookup)
{
double originalValue = lookup(data.OriginalDifficulty);
double adjustedValue = lookup(data.AdjustedDifficulty);
if (!Precision.AlmostEquals(originalValue, adjustedValue))
attributesFillFlow.Add(new AttributeDisplay(name, originalValue, adjustedValue));
}
}
public void SetContent(Data? data)
{
if (this.data == data)
return;
this.data = data;
updateDisplay();
}
protected override void PopIn() => this.FadeIn(200, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(200, Easing.OutQuint);
public void Move(Vector2 pos) => Position = pos;
public class Data
{
public BeatmapDifficulty OriginalDifficulty { get; }
public BeatmapDifficulty AdjustedDifficulty { get; }
public Data(BeatmapDifficulty originalDifficulty, BeatmapDifficulty adjustedDifficulty)
{
OriginalDifficulty = originalDifficulty;
AdjustedDifficulty = adjustedDifficulty;
}
}
private partial class AttributeDisplay : CompositeDrawable
{
public AttributeDisplay(string name, double original, double adjusted)
{
AutoSizeAxes = Axes.Both;
InternalChild = new OsuSpriteText
{
Font = OsuFont.Default.With(weight: FontWeight.Bold),
Text = $"{name}: {original:0.0#} → {adjusted:0.0#}"
};
}
}
}
}

View File

@ -3,21 +3,23 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osuTK;
using System.Threading;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
namespace osu.Game.Overlays.Mods
{
@ -25,7 +27,7 @@ namespace osu.Game.Overlays.Mods
/// On the mod select overlay, this provides a local updating view of BPM, star rating and other
/// difficulty attributes so the user can have a better insight into what mods are changing.
/// </summary>
public partial class BeatmapAttributesDisplay : ModFooterInformationDisplay
public partial class BeatmapAttributesDisplay : ModFooterInformationDisplay, IHasCustomTooltip<AdjustedAttributesTooltip.Data?>
{
private StarRatingDisplay starRatingDisplay = null!;
private BPMDisplay bpmDisplay = null!;
@ -47,9 +49,18 @@ namespace osu.Game.Overlays.Mods
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
[Resolved]
private OsuGameBase game { get; set; } = null!;
private IBindable<RulesetInfo> gameRuleset = null!;
private CancellationTokenSource? cancellationSource;
private IBindable<StarDifficulty?> starDifficulty = null!;
public ITooltip<AdjustedAttributesTooltip.Data?> GetCustomTooltip() => new AdjustedAttributesTooltip();
public AdjustedAttributesTooltip.Data? TooltipContent { get; private set; }
private const float transition_duration = 250;
[BackgroundDependencyLoader]
@ -80,8 +91,8 @@ namespace osu.Game.Overlays.Mods
{
circleSizeDisplay = new VerticalAttributeDisplay("CS") { Shear = new Vector2(-shear, 0), },
drainRateDisplay = new VerticalAttributeDisplay("HP") { Shear = new Vector2(-shear, 0), },
approachRateDisplay = new VerticalAttributeDisplay("AR") { Shear = new Vector2(-shear, 0), },
overallDifficultyDisplay = new VerticalAttributeDisplay("OD") { Shear = new Vector2(-shear, 0), },
approachRateDisplay = new VerticalAttributeDisplay("AR") { Shear = new Vector2(-shear, 0), },
});
}
@ -92,7 +103,6 @@ namespace osu.Game.Overlays.Mods
mods.BindValueChanged(_ =>
{
modSettingChangeTracker?.Dispose();
modSettingChangeTracker = new ModSettingChangeTracker(mods.Value);
modSettingChangeTracker.SettingChanged += _ => updateValues();
updateValues();
@ -107,6 +117,11 @@ namespace osu.Game.Overlays.Mods
updateCollapsedState();
});
gameRuleset = game.Ruleset.GetBoundCopy();
gameRuleset.BindValueChanged(_ => updateValues());
BeatmapInfo.BindValueChanged(_ => updateValues(), true);
updateCollapsedState();
}
@ -129,13 +144,8 @@ namespace osu.Game.Overlays.Mods
private void startAnimating()
{
Content.AutoSizeEasing = Easing.OutQuint;
Content.AutoSizeDuration = transition_duration;
}
private void updateCollapsedState()
{
RightContent.FadeTo(Collapsed.Value && !IsHovered ? 0 : 1, transition_duration, Easing.OutQuint);
LeftContent.AutoSizeEasing = Content.AutoSizeEasing = Easing.OutQuint;
LeftContent.AutoSizeDuration = Content.AutoSizeDuration = transition_duration;
}
private void updateValues() => Scheduler.AddOnce(() =>
@ -160,9 +170,18 @@ namespace osu.Game.Overlays.Mods
bpmDisplay.Current.Value = BeatmapInfo.Value.BPM * rate;
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty);
BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty);
foreach (var mod in mods.Value.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(adjustedDifficulty);
mod.ApplyToDifficulty(originalDifficulty);
Ruleset ruleset = gameRuleset.Value.CreateInstance();
BeatmapDifficulty adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(originalDifficulty, rate);
TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, adjustedDifficulty);
approachRateDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.ApproachRate, adjustedDifficulty.ApproachRate);
overallDifficultyDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.OverallDifficulty, adjustedDifficulty.OverallDifficulty);
circleSizeDisplay.Current.Value = adjustedDifficulty.CircleSize;
drainRateDisplay.Current.Value = adjustedDifficulty.DrainRate;
@ -170,6 +189,11 @@ namespace osu.Game.Overlays.Mods
overallDifficultyDisplay.Current.Value = adjustedDifficulty.OverallDifficulty;
});
private void updateCollapsedState()
{
RightContent.FadeTo(Collapsed.Value && !IsHovered ? 0 : 1, transition_duration, Easing.OutQuint);
}
private partial class BPMDisplay : RollingCounter<double>
{
protected override double RollingDuration => 500;

View File

@ -115,6 +115,7 @@ namespace osu.Game.Overlays.Mods
public IEnumerable<ModState> AllAvailableMods => AvailableMods.Value.SelectMany(pair => pair.Value);
private readonly BindableBool customisationVisible = new BindableBool();
private Bindable<bool> textSearchStartsActive = null!;
private ModSettingsArea modSettingsArea = null!;
private ColumnScrollContainer columnScroll = null!;
@ -154,7 +155,7 @@ namespace osu.Game.Overlays.Mods
}
[BackgroundDependencyLoader]
private void load(OsuGameBase game, OsuColour colours, AudioManager audio)
private void load(OsuGameBase game, OsuColour colours, AudioManager audio, OsuConfigManager configManager)
{
Header.Title = ModSelectOverlayStrings.ModSelectTitle;
Header.Description = ModSelectOverlayStrings.ModSelectDescription;
@ -282,6 +283,8 @@ namespace osu.Game.Overlays.Mods
}
globalAvailableMods.BindTo(game.AvailableMods);
textSearchStartsActive = configManager.GetBindable<bool>(OsuSetting.ModSelectTextSearchStartsActive);
}
public override void Hide()
@ -617,6 +620,9 @@ namespace osu.Game.Overlays.Mods
nonFilteredColumnCount += 1;
}
if (textSearchStartsActive.Value)
SearchTextBox.TakeFocus();
}
protected override void PopOut()

View File

@ -1,15 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Mods;
using osuTK.Graphics;
namespace osu.Game.Overlays.Mods
{
@ -23,11 +28,45 @@ namespace osu.Game.Overlays.Mods
private readonly BindableWithCurrent<double> current = new BindableWithCurrent<double>();
public Bindable<ModEffect> AdjustType = new Bindable<ModEffect>();
/// <summary>
/// Text to display in the top area of the display.
/// </summary>
public LocalisableString Label { get; protected set; }
private readonly EffectCounter counter;
private readonly OsuSpriteText text;
[Resolved]
private OsuColour colours { get; set; } = null!;
private void updateTextColor()
{
Color4 newColor;
switch (AdjustType.Value)
{
case ModEffect.NotChanged:
newColor = Color4.White;
break;
case ModEffect.DifficultyReduction:
newColor = colours.ForModType(ModType.DifficultyReduction);
break;
case ModEffect.DifficultyIncrease:
newColor = colours.ForModType(ModType.DifficultyIncrease);
break;
default:
throw new ArgumentOutOfRangeException(nameof(AdjustType.Value));
}
text.Colour = newColor;
counter.Colour = newColor;
}
public VerticalAttributeDisplay(LocalisableString label)
{
Label = label;
@ -37,15 +76,18 @@ namespace osu.Game.Overlays.Mods
Origin = Anchor.CentreLeft;
Anchor = Anchor.CentreLeft;
AdjustType.BindValueChanged(_ => updateTextColor());
InternalChild = new FillFlowContainer
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
AutoSizeAxes = Axes.Y,
Width = 50,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new OsuSpriteText
text = new OsuSpriteText
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
@ -53,7 +95,7 @@ namespace osu.Game.Overlays.Mods
Margin = new MarginPadding { Horizontal = 15 }, // to reserve space for 0.XX value
Font = OsuFont.Default.With(size: 20, weight: FontWeight.Bold)
},
new EffectCounter
counter = new EffectCounter
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
@ -63,11 +105,28 @@ namespace osu.Game.Overlays.Mods
};
}
public static ModEffect CalculateEffect(double oldValue, double newValue)
{
if (Precision.AlmostEquals(newValue, oldValue, 0.01))
return ModEffect.NotChanged;
if (newValue < oldValue)
return ModEffect.DifficultyReduction;
return ModEffect.DifficultyIncrease;
}
public enum ModEffect
{
NotChanged,
DifficultyReduction,
DifficultyIncrease,
}
private partial class EffectCounter : RollingCounter<double>
{
protected override double RollingDuration => 500;
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString("0.0");
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString("0.0#");
protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText
{

View File

@ -23,6 +23,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
{
LabelText = GeneralSettingsStrings.LanguageDropdown,
Current = game.CurrentLanguage,
AlwaysShowSearchBar = true,
},
new SettingsCheckbox
{

View File

@ -1,14 +1,13 @@
// 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.
#nullable disable
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.Configuration;
@ -16,23 +15,27 @@ using osu.Game.Localisation;
using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.Settings.Sections.Maintenance;
using osu.Game.Updater;
using SharpCompress.Archives.Zip;
namespace osu.Game.Overlays.Settings.Sections.General
{
public partial class UpdateSettings : SettingsSubsection
{
[Resolved(CanBeNull = true)]
private UpdateManager updateManager { get; set; }
protected override LocalisableString Header => GeneralSettingsStrings.UpdateHeader;
private SettingsButton checkForUpdatesButton;
private SettingsButton checkForUpdatesButton = null!;
[Resolved(CanBeNull = true)]
private INotificationOverlay notifications { get; set; }
[Resolved]
private UpdateManager? updateManager { get; set; }
[BackgroundDependencyLoader(true)]
private void load(Storage storage, OsuConfigManager config, OsuGame game)
[Resolved]
private INotificationOverlay? notifications { get; set; }
[Resolved]
private Storage storage { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, OsuGame? game)
{
Add(new SettingsEnumDropdown<ReleaseStream>
{
@ -54,7 +57,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
{
notifications?.Post(new SimpleNotification
{
Text = GeneralSettingsStrings.RunningLatestRelease(game.Version),
Text = GeneralSettingsStrings.RunningLatestRelease(game!.Version),
Icon = FontAwesome.Solid.CheckCircle,
});
}
@ -74,6 +77,13 @@ namespace osu.Game.Overlays.Settings.Sections.General
Action = () => storage.PresentExternally(),
});
Add(new SettingsButton
{
Text = GeneralSettingsStrings.ExportLogs,
Keywords = new[] { @"bug", "report", "logs", "files" },
Action = () => Task.Run(exportLogs),
});
Add(new SettingsButton
{
Text = GeneralSettingsStrings.ChangeFolderLocation,
@ -81,5 +91,44 @@ namespace osu.Game.Overlays.Settings.Sections.General
});
}
}
private void exportLogs()
{
ProgressNotification notification = new ProgressNotification
{
State = ProgressNotificationState.Active,
Text = "Exporting logs...",
};
notifications?.Post(notification);
const string archive_filename = "exports/compressed-logs.zip";
try
{
var logStorage = Logger.Storage;
using (var outStream = storage.CreateFileSafely(archive_filename))
using (var zip = ZipArchive.Create())
{
foreach (string? f in logStorage.GetFiles(string.Empty, "*.log")) zip.AddEntry(f, logStorage.GetStream(f), true);
zip.SaveTo(outStream);
}
}
catch
{
notification.State = ProgressNotificationState.Cancelled;
// cleanup if export is failed or canceled.
storage.Delete(archive_filename);
throw;
}
notification.CompletionText = "Exported logs! Click to view.";
notification.CompletionClickAction = () => storage.PresentFileExternally(archive_filename);
notification.State = ProgressNotificationState.Completed;
}
}
}

View File

@ -1,11 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Localisation;
using osu.Framework.Screens;
using osu.Game.Database;
namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
@ -13,9 +15,12 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
private readonly TaskCompletionSource<string> taskCompletionSource;
[Resolved]
private LegacyImportManager legacyImportManager { get; set; } = null!;
protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.Disabled;
protected override bool IsValidDirectory(DirectoryInfo? info) => info?.GetFiles("osu!.*.cfg").Any() ?? false;
protected override bool IsValidDirectory(DirectoryInfo? info) => legacyImportManager.IsUsableForStableImport(info, out _);
public override LocalisableString HeaderText => "Please select your osu!stable install location";
@ -26,7 +31,10 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
protected override void OnSelection(DirectoryInfo directory)
{
taskCompletionSource.TrySetResult(directory.FullName);
if (!legacyImportManager.IsUsableForStableImport(directory, out var stableRoot))
throw new InvalidOperationException($@"{nameof(OnSelection)} was called on an invalid directory. This should never happen.");
taskCompletionSource.TrySetResult(stableRoot.FullName);
this.Exit();
}

View File

@ -42,6 +42,12 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
ClassicDefault = ModSelectHotkeyStyle.Classic
},
new SettingsCheckbox
{
LabelText = UserInterfaceStrings.ModSelectTextSearchStartsActive,
Current = config.GetBindable<bool>(OsuSetting.ModSelectTextSearchStartsActive),
ClassicDefault = false
},
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.BackgroundBlur,
Current = config.GetBindable<bool>(OsuSetting.SongSelectBackgroundBlur),

View File

@ -17,6 +17,7 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens;
@ -58,6 +59,9 @@ namespace osu.Game.Overlays.SkinEditor
[Resolved]
private Bindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
[Resolved]
private Bindable<RulesetInfo> ruleset { get; set; } = null!;
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
@ -153,7 +157,11 @@ namespace osu.Game.Overlays.SkinEditor
if (screen is Player)
return;
var replayGeneratingMod = beatmap.Value.BeatmapInfo.Ruleset.CreateInstance().GetAutoplayMod();
// the validity of the current game-wide beatmap + ruleset combination is enforced by song select.
// if we're anywhere else, the state is unknown and may not make sense, so forcibly set something that does.
if (screen is not PlaySongSelect)
ruleset.Value = beatmap.Value.BeatmapInfo.Ruleset;
var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod();
IReadOnlyList<Mod> usableMods = mods.Value;

View File

@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mods
public override string Acronym => "CL";
public override double ScoreMultiplier => 1;
public override double ScoreMultiplier => 0.5;
public override IconUsage? Icon => FontAwesome.Solid.History;

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mods
public override string Name => "Synesthesia";
public override string Acronym => "SY";
public override LocalisableString Description => "Colours hit objects based on the rhythm.";
public override double ScoreMultiplier => 1;
public override double ScoreMultiplier => 0.8;
public override ModType Type => ModType.Fun;
}
}

View File

@ -32,9 +32,9 @@ namespace osu.Game.Rulesets.Mods
value -= 1;
if (SpeedChange.Value >= 1)
value /= 5;
return 1 + value;
return 1 + value / 5;
else
return 0.6 + value;
}
}

View File

@ -377,6 +377,17 @@ namespace osu.Game.Rulesets
/// <returns>The display name.</returns>
public virtual LocalisableString GetDisplayNameForHitResult(HitResult result) => result.GetLocalisableDescription();
/// <summary>
/// Applies changes to difficulty attributes for presenting to a user a rough estimate of how rate adjust mods affect difficulty.
/// Importantly, this should NOT BE USED FOR ANY CALCULATIONS.
///
/// It is also not always correct, and arguably is never correct depending on your frame of mind.
/// </summary>
/// <param name="difficulty">>The <see cref="IBeatmapDifficultyInfo"/> that will be adjusted.</param>
/// <param name="rate">The rate adjustment multiplier from mods. For example 1.5 for DT.</param>
/// <returns>The adjusted difficulty attributes.</returns>
public virtual BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate) => new BeatmapDifficulty(difficulty);
/// <summary>
/// Creates ruleset-specific beatmap filter criteria to be used on the song select screen.
/// </summary>

View File

@ -139,9 +139,12 @@ namespace osu.Game.Rulesets.Scoring
/// <summary>
/// A special result used as a padding value for legacy rulesets. It is a hit type and affects combo, but does not affect the base score (does not affect accuracy).
///
/// DO NOT USE FOR ANYTHING EVER.
/// </summary>
/// <remarks>
/// DO NOT USE.
/// This is used when dealing with legacy scores, which historically only have counts stored for 300/100/50/miss.
/// For these scores, we pad the hit statistics with `LegacyComboIncrease` to meet the correct max combo for the score.
/// </remarks>
[EnumMember(Value = "legacy_combo_increase")]
[Order(99)]

View File

@ -62,8 +62,8 @@ namespace osu.Game.Rulesets.Scoring.Legacy
SourceRuleset = beatmapInfo.Ruleset,
CircleSize = beatmapInfo.Difficulty.CircleSize,
OverallDifficulty = beatmapInfo.Difficulty.OverallDifficulty,
EndTimeObjectCount = beatmapInfo.Difficulty.EndTimeObjectCount,
TotalObjectCount = beatmapInfo.Difficulty.TotalObjectCount
EndTimeObjectCount = beatmapInfo.EndTimeObjectCount,
TotalObjectCount = beatmapInfo.TotalObjectCount
};
}
}

View File

@ -17,6 +17,7 @@ namespace osu.Game.Screens.Play
/// Encapsulates gameplay timing logic and provides a <see cref="IGameplayClock"/> via DI for gameplay components to use.
/// </summary>
[Cached(typeof(IGameplayClock))]
[Cached(typeof(GameplayClockContainer))]
public partial class GameplayClockContainer : Container, IAdjustableClock, IGameplayClock
{
public IBindable<bool> IsPaused => isPaused;

View File

@ -212,6 +212,13 @@ namespace osu.Game.Screens.Play.HUD
glowBar.Alpha = (float)Interpolation.DampContinuously(glowBar.Alpha, GlowBarValue > 0 ? 1 : 0, 40, Time.Elapsed);
}
protected override void FinishInitialAnimation(double value)
{
base.FinishInitialAnimation(value);
this.TransformTo(nameof(HealthBarValue), value, 500, Easing.OutQuint);
this.TransformTo(nameof(GlowBarValue), value, 250, Easing.OutQuint);
}
protected override void Flash()
{
base.Flash();

View File

@ -58,7 +58,9 @@ namespace osu.Game.Screens.Play.HUD
health = HealthProcessor.Health.GetBoundCopy();
health.BindValueChanged(h =>
{
finishInitialAnimation();
if (initialIncrease != null)
FinishInitialAnimation(h.OldValue);
Current.Value = h.NewValue;
});
@ -90,16 +92,16 @@ namespace osu.Game.Screens.Play.HUD
Scheduler.AddOnce(Flash);
if (newValue >= health.Value)
finishInitialAnimation();
FinishInitialAnimation(health.Value);
}, increase_delay, true);
}
private void finishInitialAnimation()
protected virtual void FinishInitialAnimation(double value)
{
if (initialIncrease == null)
return;
initialIncrease?.Cancel();
initialIncrease.Cancel();
initialIncrease = null;
// aside from the repeating `initialIncrease` scheduled task,

View File

@ -485,7 +485,14 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
}
}
public override void Clear() => judgementsContainer.Clear();
public override void Clear()
{
foreach (var j in judgementsContainer)
{
j.ClearTransforms();
j.Expire();
}
}
public enum CentreMarkerStyles
{

View File

@ -63,7 +63,14 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
judgementsFlow.Push(GetColourForHitResult(judgement.Type));
}
public override void Clear() => judgementsFlow.Clear();
public override void Clear()
{
foreach (var j in judgementsFlow)
{
j.ClearTransforms();
j.Expire();
}
}
private partial class JudgementFlow : FillFlowContainer<HitErrorShape>
{

View File

@ -25,6 +25,8 @@ namespace osu.Game.Screens.Play
private readonly Score score;
public override bool AllowBackButton => true;
protected override bool CheckModsAllowFailure()
{
if (!allowFail)

View File

@ -245,6 +245,9 @@ namespace osu.Game.Screens.Select.Carousel
private void updateKeyCount()
{
if (Item?.State.Value == CarouselItemState.Collapsed)
return;
if (ruleset.Value.OnlineID == 3)
{
// Account for mania differences locally for now.

View File

@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@ -25,10 +26,11 @@ using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osu.Game.Overlays.Mods;
namespace osu.Game.Screens.Select.Details
{
public partial class AdvancedStats : Container
public partial class AdvancedStats : Container, IHasCustomTooltip<AdjustedAttributesTooltip.Data>
{
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; }
@ -44,6 +46,9 @@ namespace osu.Game.Screens.Select.Details
protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate;
private readonly StatisticRow starDifficulty;
public ITooltip<AdjustedAttributesTooltip.Data> GetCustomTooltip() => new AdjustedAttributesTooltip();
public AdjustedAttributesTooltip.Data TooltipContent { get; private set; }
private IBeatmapInfo beatmapInfo;
public IBeatmapInfo BeatmapInfo
@ -118,15 +123,28 @@ namespace osu.Game.Screens.Select.Details
IBeatmapDifficultyInfo baseDifficulty = BeatmapInfo?.Difficulty;
BeatmapDifficulty adjustedDifficulty = null;
if (baseDifficulty != null && mods.Value.Any(m => m is IApplicableToDifficulty))
IRulesetInfo ruleset = gameRuleset?.Value ?? beatmapInfo.Ruleset;
if (baseDifficulty != null)
{
adjustedDifficulty = new BeatmapDifficulty(baseDifficulty);
BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(baseDifficulty);
foreach (var mod in mods.Value.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(adjustedDifficulty);
}
mod.ApplyToDifficulty(originalDifficulty);
IRulesetInfo ruleset = gameRuleset?.Value ?? beatmapInfo.Ruleset;
adjustedDifficulty = originalDifficulty;
if (gameRuleset != null)
{
double rate = 1;
foreach (var mod in mods.Value.OfType<IApplicableToRate>())
rate = mod.ApplyToRate(0, rate);
adjustedDifficulty = ruleset.CreateInstance().GetRateAdjustedDisplayDifficulty(originalDifficulty, rate);
TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, adjustedDifficulty);
}
}
switch (ruleset.OnlineID)
{

View File

@ -37,7 +37,7 @@
</PackageReference>
<PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.1213.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1127.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1215.0" />
<PackageReference Include="Sentry" Version="3.40.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
<PackageReference Include="SharpCompress" Version="0.33.0" />