1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-24 06:19:55 +08:00

Compare commits

..

418 Commits

265 changed files with 6554 additions and 1570 deletions
+5 -15
View File
@@ -46,22 +46,16 @@ body:
value: |
## Logs
Attaching log files is required for every reported bug. See instructions below on how to find them.
**Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead.
Attaching log files is required for **every** issue, regardless of whether you deem them required or not. See instructions below on how to find them.
### Desktop platforms
If the game has not yet been closed since you found the bug:
1. Head on to game settings and click on "Open osu! folder"
2. Then open the `logs` folder located there
1. Head on to game settings and click on "Export logs"
2. Click the notification to locate the file
3. Drag the generated `.zip` files into the github issue window
The default places to find the logs on desktop platforms are as follows:
- `%AppData%/osu/logs` *on Windows*
- `~/.local/share/osu/logs` *on Linux*
- `~/Library/Application Support/osu/logs` *on macOS*
If you have selected a custom location for the game files, you can find the `logs` folder there.
![export logs button](https://github.com/ppy/osu/assets/191335/cbfa5550-b7ed-4c5c-8dd0-8b87cc90ad9b)
### Mobile platforms
@@ -69,10 +63,6 @@ body:
- *On Android*, navigate to `Android/data/sh.ppy.osulazer/files/logs` using a file browser app.
- *On iOS*, connect your device to a PC and copy the `logs` directory from the app's document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
---
After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below.
- type: textarea
attributes:
label: Logs
+1 -1
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.1219.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
@@ -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)
@@ -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));
}
}
}
@@ -11,7 +11,7 @@ using osuTK;
namespace osu.Game.Rulesets.Catch.Tests.Mods
{
public partial class TestSceneCatchModPerfect : ModPerfectTestScene
public partial class TestSceneCatchModPerfect : ModFailConditionTestScene
{
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
File diff suppressed because one or more lines are too long
@@ -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
@@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Tests
AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash);
for (int i = 0; i < 9; i++)
for (int i = 0; i < 11; i++)
{
int count = i + 1;
AddUntilStep($"wait for hyperdash #{count}", () => hyperDashCount >= count);
@@ -104,12 +104,22 @@ namespace osu.Game.Rulesets.Catch.Tests
})
}, 1);
createObjects(() => new Fruit { X = right_x }, count: 2, spacing: 0, spacingAfterGroup: 400);
createObjects(() => new TestJuiceStream(left_x)
{
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero),
new PathControlPoint(new Vector2(0, 300))
})
}, count: 1, spacingAfterGroup: 150);
createObjects(() => new Fruit { X = left_x }, count: 1, spacing: 0, spacingAfterGroup: 400);
createObjects(() => new Fruit { X = right_x }, count: 2, spacing: 0);
return beatmap;
void createObjects(Func<CatchHitObject> createObject, int count = 3)
void createObjects(Func<CatchHitObject> createObject, int count = 3, float spacing = 140, float spacingAfterGroup = 700)
{
const float spacing = 140;
for (int i = 0; i < count; i++)
{
var hitObject = createObject();
@@ -117,7 +127,7 @@ namespace osu.Game.Rulesets.Catch.Tests
beatmap.HitObjects.Add(hitObject);
}
startTime += 700;
startTime += spacingAfterGroup;
}
}
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Catch.Beatmaps
{
@@ -38,5 +39,25 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
}
};
}
/// <summary>
/// Enumerate all <see cref="PalpableCatchHitObject"/>s, sorted by their start times.
/// </summary>
/// <remarks>
/// If multiple objects have the same start time, the ordering is preserved (it is a stable sorting).
/// </remarks>
public static IEnumerable<PalpableCatchHitObject> GetPalpableObjects(IEnumerable<HitObject> hitObjects)
{
return hitObjects.SelectMany(selectPalpableObjects).OrderBy(h => h.StartTime);
IEnumerable<PalpableCatchHitObject> selectPalpableObjects(HitObject h)
{
if (h is PalpableCatchHitObject palpable)
yield return palpable;
foreach (var nested in h.NestedHitObjects.OfType<PalpableCatchHitObject>())
yield return nested;
}
}
}
}
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
@@ -208,24 +207,9 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
private static void initialiseHyperDash(IBeatmap beatmap)
{
List<PalpableCatchHitObject> palpableObjects = new List<PalpableCatchHitObject>();
foreach (var currentObject in beatmap.HitObjects)
{
if (currentObject is Fruit fruitObject)
palpableObjects.Add(fruitObject);
if (currentObject is JuiceStream)
{
foreach (var juice in currentObject.NestedHitObjects)
{
if (juice is PalpableCatchHitObject palpableObject && !(juice is TinyDroplet))
palpableObjects.Add(palpableObject);
}
}
}
palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
var palpableObjects = CatchBeatmap.GetPalpableObjects(beatmap.HitObjects)
.Where(h => h is Fruit || (h is Droplet && h is not TinyDroplet))
.ToArray();
double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) / 2;
@@ -237,7 +221,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
int lastDirection = 0;
double lastExcess = halfCatcherWidth;
for (int i = 0; i < palpableObjects.Count - 1; i++)
for (int i = 0; i < palpableObjects.Length - 1; i++)
{
var currentObject = palpableObjects[i];
var nextObject = palpableObjects[i + 1];
+13
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;
}
}
}
@@ -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)
@@ -7,7 +7,7 @@ using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
internal class CatchLegacyScoreSimulator : ILegacyScoreSimulator
{
private readonly ScoreProcessor scoreProcessor = new CatchScoreProcessor();
private int legacyBonusScore;
private int standardisedBonusScore;
private int combo;
@@ -74,6 +76,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
simulateHit(obj, ref attributes);
attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore;
attributes.BonusScore = legacyBonusScore;
attributes.MaxCombo = combo;
return attributes;
}
@@ -132,7 +136,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
if (isBonus)
{
legacyBonusScore += scoreIncrease;
standardisedBonusScore += Judgement.ToNumericResult(bonusResult);
standardisedBonusScore += scoreProcessor.GetBaseScoreForResult(bonusResult);
}
else
attributes.AccuracyScore += scoreIncrease;
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
private double placementStartTime;
private double placementEndTime;
protected override bool IsValidForPlacement => HitObject.Duration > 0;
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
public BananaShowerPlacementBlueprint()
{
@@ -4,6 +4,7 @@
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
@@ -24,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
private InputManager inputManager = null!;
protected override bool IsValidForPlacement => HitObject.Duration > 0;
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
public JuiceStreamPlacementBlueprint()
{
@@ -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.
@@ -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
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Scoring
}
protected override double GetComboScoreChange(JudgementResult result)
=> Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
=> GetBaseScoreForResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
public override ScoreRank RankFromAccuracy(double accuracy)
{
+19 -7
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;
}
}
@@ -18,10 +18,13 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestFixture]
public class ManiaBeatmapConversionTest : BeatmapConversionTest<ManiaConvertMapping, ConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania.Tests";
[TestCase("basic")]
[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)
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestFixture]
public class ManiaBeatmapSampleConversionTest : BeatmapConversionTest<ConvertMapping<SampleConvertValue>, SampleConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania.Tests";
[TestCase("convert-samples")]
[TestCase("mania-samples")]
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
public class ManiaDifficultyCalculatorTest : DifficultyCalculatorTest
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania.Tests";
[TestCase(2.3493769750220914d, 242, "diffcalc-test")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
@@ -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
});
}
}
}
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
@@ -25,8 +26,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
public void TestHitWindowWithoutDoubleTime() => CreateModTest(new ModTestData
{
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0
&& Player.ScoreProcessor.Accuracy.Value == 1
&& Player.ScoreProcessor.TotalScore.Value == 1_000_000,
&& Precision.AlmostEquals(Player.ScoreProcessor.Accuracy.Value, 0.9836, 0.01)
&& Player.ScoreProcessor.TotalScore.Value == 946_049,
Autoplay = false,
Beatmap = new Beatmap
{
@@ -53,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
Mod = doubleTime,
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0
&& Player.ScoreProcessor.Accuracy.Value == 1
&& Player.ScoreProcessor.TotalScore.Value == (long)(1_000_010 * doubleTime.ScoreMultiplier),
&& Player.ScoreProcessor.TotalScore.Value == (long)(1_000_000 * doubleTime.ScoreMultiplier),
Autoplay = false,
Beatmap = new Beatmap
{
@@ -1,14 +1,19 @@
// 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;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public partial class TestSceneManiaModPerfect : ModPerfectTestScene
public partial class TestSceneManiaModPerfect : ModFailConditionTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
@@ -24,5 +29,52 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
[TestCase(false)]
[TestCase(true)]
public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss);
[Test]
public void TestGreatHit() => CreateModTest(new ModTestData
{
Mod = new ManiaModPerfect(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Note
{
StartTime = 1000,
}
},
},
ReplayFrames = new List<ReplayFrame>
{
new ManiaReplayFrame(1020, ManiaAction.Key1),
new ManiaReplayFrame(2000)
}
});
[Test]
public void TestBreakOnHoldNote() => CreateModTest(new ModTestData
{
Mod = new ManiaModPerfect(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true) && Player.Results.Count == 2,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HoldNote
{
StartTime = 1000,
EndTime = 3000,
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new ManiaReplayFrame(1000, ManiaAction.Key1),
new ManiaReplayFrame(2000)
}
});
}
}
@@ -0,0 +1,72 @@
// 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;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public partial class TestSceneManiaModSuddenDeath : ModFailConditionTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
public TestSceneManiaModSuddenDeath()
: base(new ManiaModSuddenDeath())
{
}
[Test]
public void TestGreatHit() => CreateModTest(new ModTestData
{
Mod = new ManiaModSuddenDeath(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Note
{
StartTime = 1000,
}
},
},
ReplayFrames = new List<ReplayFrame>
{
new ManiaReplayFrame(1020, ManiaAction.Key1),
new ManiaReplayFrame(2000)
}
});
[Test]
public void TestBreakOnHoldNote() => CreateModTest(new ModTestData
{
Mod = new ManiaModSuddenDeath(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true) && Player.Results.Count == 2,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HoldNote
{
StartTime = 1000,
EndTime = 3000,
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new ManiaReplayFrame(1000, ManiaAction.Key1),
new ManiaReplayFrame(2000)
}
});
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,449 @@
osu file format v9
[General]
StackLeniency: 0.4
Mode: 0
[Difficulty]
HPDrainRate:5
CircleSize:4
OverallDifficulty:5
ApproachRate:6
SliderMultiplier:1.7
SliderTickRate:2
[Events]
//Background and Video events
//Break Periods
2,98678,112295
2,185757,200967
//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]
695,530.973451327434,4,2,1,20,1,0
33457,-100,4,2,1,25,0,0
33988,-100,4,2,1,30,0,0
34386,-100,4,1,0,30,0,0
38649,-100,4,1,1,30,0,0
42897,-100,4,1,0,30,0,0
47144,-100,4,1,1,30,0,0
51530,-100,4,2,1,20,0,0
56978,571.428571428571,4,2,1,20,1,0
58692,845.070422535211,4,2,1,20,1,0
60248,530.973451327434,4,2,1,20,1,0
60740,-100,4,1,1,30,0,0
61555,-66.6666666666667,4,1,1,30,0,0
62219,-100,4,1,0,40,0,0
78148,-100,4,1,0,30,0,0
78413,-100,4,1,0,35,0,0
78679,-100,4,1,0,40,0,0
78944,-100,4,1,0,45,0,0
79210,-100,4,1,0,40,0,0
96466,-100,4,2,1,30,0,0
132285,-100,4,2,1,20,0,0
149453,-100,4,1,1,35,0,0
153790,-100,4,2,1,40,0,0
157639,-100,4,1,1,35,0,0
162020,-100,4,2,1,40,0,0
166158,-100,4,1,0,40,0,0
201733,-100,4,2,1,20,0,0
219099,-133.333333333333,4,2,1,20,0,0
221024,-100,4,1,1,30,0,0
221290,-100,4,1,0,30,0,0
[HitObjects]
256,192,15562,12,0,17155
72,120,17686,5,8
128,224,17951,1,0
185,119,18217,1,0
246,220,18482,1,0
128,224,18748,2,0,B|161:262|208:264,1,85,4|0
309,213,19279,2,0,B|297:169|325:120,2,85,0|0|8
309,213,20075,5,0
309,332,20341,1,0
206,272,20606,1,8
309,213,20871,2,0,B|336:117|261:56,1,170,4|0
205,272,21933,6,0,B|183:307|125:328,1,85,8|0
149,256,22464,2,0,B|114:281|45:280,1,85,0|0
101,216,22995,2,0,B|16:264|-56:176|16:72|104:128,1,255,4|0
149,136,24057,6,0,B|170:100|229:80,1,85,8|0
205,149,24588,2,0,B|239:123|309:125,1,85,0|8
253,189,25119,2,0,B|349:144|413:221,1,170,4|8
240,336,26181,5,8
288,264,26447,1,0
344,328,26712,2,0,B|391:339|440:328,1,85,0|0
488,270,27243,2,0,B|424:256|392:200,1,85,4|0
329,230,27774,2,0,B|328:176|386:142,1,85,0|0
363,69,28305,2,0,B|328:40|280:56,2,85,8|0|0
312,136,29102,1,0
224,120,29367,2,0,B|192:168|256:240|224:296,1,170,4|8
96,240,30429,6,0,B|83:195|56:160,1,85,8|0
96,88,30960,2,0,B|83:132|56:168,1,85,0|0
59,164,31491,2,0,B|129:182|187:167|254:149|323:168,1,255,4|0
312,165,32553,6,0,B|302:210|256:237,1,85,8|0
312,166,33084,2,0,B|321:120|368:94,1,85,8|0
312,166,33615,2,0,B|318:204|374:193|426:183|450:247,1,170,8|8
200,232,34677,5,4
119,169,34942,1,0
57,248,35208,1,8
137,311,35473,1,0
200,232,35739,5,0
248,302,36004,1,0
318,254,36270,1,8
270,183,36535,1,0
200,232,36801,6,0,B|120:272|120:272|40:224,1,170,0|8
130,183,37597,1,0
200,232,37863,2,0,B|280:192|280:192|368:240,1,170,0|8
167,111,38925,6,0,B|134:71|98:65,1,85,8|0
167,112,39456,2,0,B|115:116|90:142,1,85,4|0
167,112,39987,2,0,B|120:192|176:248|240:312|152:368,1,255,8|0
173,351,41048,6,0,B|142:305|80:288,1,85,8|0
173,351,41579,2,0,B|194:299|175:238,1,85,4|0
173,351,42110,2,0,B|237:351|253:303|269:255|341:263,1,170,8|8
128,144,43172,5,4
208,176,43438,1,0
288,144,43703,1,8
368,176,43969,1,0
408,272,44234,5,0
312,312,44500,1,0
216,272,44765,1,8
120,312,45031,1,0
48,240,45296,5,0
160,272,45562,1,0
272,240,45827,1,8
384,280,46093,1,0
496,240,46358,2,0,B|448:208|448:208|496:176|504:128|442:127,1,170,0|8
152,128,47420,6,0,B|122:167|120:224,1,85,8|0
88,128,47951,2,0,B|95:177|133:218,1,85,4|0
121,204,48482,2,0,B|140:296|264:280|308:368,1,255,8|0
308,368,49544,6,0,B|293:318|324:264,1,85,8|0
368,348,50075,2,0,B|322:323|305:263,1,85,4|0
324,200,50606,2,0,B|274:214|203:224|142:108|131:56|243:32|243:120|211:160|107:136,1,340,8|2
369,216,52730,5,2
176,312,53792,2,0,B|166:217|64:144,1,170,0|0
179,150,54588,1,0
120,88,54854,2,0,B|107:176|38:232,1,170,2|0
464,320,55916,6,0,B|392:252|288:280,1,170,0|0
280,104,56978,6,0,B|312:192|416:208,1,170,2|0
192,160,58120,2,0,B|182:224|112:240,1,85,2|0
24,240,58692,6,0,B|72:240|88:272,1,56.6666666666667,6|0
224,296,59325,2,0,B|240:200|200:120,1,170
316,136,60513,5,0
400,156,60778,2,0,B|408:100|364:56,1,85,10|0
320,16,61309,1,2
160,112,61840,6,0,B|95:104|28:135,1,127.499996200204,8|0
160,112,62371,6,0,B|80:168|96:296,1,170,4|8
176,280,63168,1,0
224,208,63433,2,0,B|280:288|392:264,1,170,0|8
456,184,64230,1,0
328,144,64495,1,8
416,248,64761,1,0
408,112,65026,1,8
336,232,65292,1,0
388,182,65557,1,8
256,288,66088,5,8
256,288,66354,1,0
256,288,66619,2,0,B|200:360|72:368,1,170,0|8
44,308,67416,1,0
87,234,67681,2,0,B|163:279|207:386,1,170,0|8
256,288,68478,1,0
400,120,68743,5,8
328,256,69009,1,0
400,120,69274,1,8
264,184,69540,1,0
400,120,69805,1,8
400,120,70336,6,0,B|395:173|368:200,1,85,8|0
213,255,70867,2,0,B|279:198|383:198,1,170,4|8
329,125,71663,1,0
248,104,71929,2,0,B|184:168|80:152,1,170,0|8
200,224,72725,1,0
272,339,72991,5,8
151,276,73256,1,0
267,204,73522,1,8
204,322,73787,1,0
287,272,74053,1,8
287,272,74584,6,0,B|336:256|368:208,1,85,8|0
372,140,75115,2,0,B|323:206|324:308,1,170,0|8
240,288,75911,1,0
160,248,76177,2,0,B|216:176|320:216,1,170,0|8
272,136,76973,1,0
200,88,77239,6,0,B|216:136|192:176,1,85,8|0
160,248,77770,2,0,B|160:296|208:320,1,85,8|0
328,232,78301,5,0
233,133,78566,1,8
297,15,78832,1,8
432,40,79097,1,8
453,176,79363,6,0,B|448:240|384:272|328:232,1,170,4|8
286,306,80159,1,0
203,288,80424,2,0,B|208:224|272:192|328:232,1,170,0|8
404,231,81221,1,0
408,160,81486,5,8
360,288,81752,1,0
472,216,82017,1,8
336,208,82283,1,0
440,296,82548,1,8
288,320,83079,5,8
288,320,83345,1,0
288,320,83610,2,0,B|200:314|128:248,1,170,0|8
88,320,84407,1,0
56,240,84672,2,0,B|133:287|176:392,1,170,0|8
163,274,85469,1,0
296,216,85734,5,8
165,75,86000,1,0
99,178,86265,1,8
282,97,86531,1,0
184,264,86796,1,8
184,264,87327,6,0,B|159:295|110:299,1,85,8|0
23,247,87858,2,0,B|91:300|192:261,1,170,4|8
245,326,88655,1,0
293,254,88920,2,0,B|213:198|109:246,1,170,0|8
181,302,89717,1,0
165,166,89982,5,8
141,302,90247,1,0
205,182,90513,1,8
109,278,90778,1,0
229,214,91044,1,8
376,132,91575,6,0,B|424:140|464:100,1,85,8|0
464,192,92106,2,0,B|456:280|352:320,1,170,0|8
300,256,92902,1,0
228,212,93168,2,0,B|268:116|164:60,1,170,0|8
100,32,93964,1,0
84,116,94230,2,0,B|116:156|108:212,1,85,8|0
188,160,94761,2,0,B|188:208|232:244,1,85,8|0
296,196,95292,2,0,B|320:236|349:239|399:242|379:198|379:198|334:185|358:245|368:276|440:260|480:316|416:356,1,340,8|4
256,192,96486,12,8,98478
264,192,113345,5,8
264,192,113876,1,8
264,192,114407,5,0
172,236,114672,1,8
184,336,114938,1,0
284,356,115203,1,8
340,268,115469,1,8
304,100,116000,1,8
304,100,116531,1,0
272,336,117062,5,8
248,200,117327,1,0
376,152,117593,1,8
376,152,118124,1,8
376,152,118655,5,0
240,128,118920,1,8
376,192,119186,1,0
496,152,119451,1,8
376,224,119717,1,8
376,224,120247,1,8
376,224,120778,1,0
376,224,121309,5,8
264,296,121575,1,0
256,160,121840,1,8
256,160,122371,1,8
256,160,122902,1,0
256,160,123433,5,8
168,264,123699,1,0
312,280,123964,1,8
312,280,124495,1,8
312,280,125026,1,0
312,280,125557,5,8
200,200,125823,1,0
312,280,126088,1,8
312,280,126619,1,8
312,280,127150,5,0
416,200,127416,1,8
432,336,127681,1,0
416,200,127947,1,8
312,280,128212,1,8
312,280,128743,1,8
312,280,129274,5,8
264,152,129540,1,8
136,192,129805,1,8
184,320,130071,1,12
88,120,132460,6,0,B|127:224|104:304,1,170,2|0
424,264,133522,2,0,B|384:159|408:80,1,170
448,168,134318,2,0,B|369:240|297:240,1,170,4|0
301,158,135115,2,0,B|277:206|309:262,1,85
395,295,135646,2,0,B|323:263|227:287,1,170,0|2
176,88,136708,6,0,B|134:57|80:64,1,85
176,88,137239,2,0,B|221:64|264:64,1,85,8|0
176,88,137770,2,0,B|137:175|196:220|272:272|208:344,1,255,4|0
136,328,138832,6,0,B|83:306|40:328,1,85
136,328,139363,2,0,B|184:312|224:328,1,85,2|0
300,296,139894,2,0,B|300:198|388:200|468:200|452:104,1,255,4|0
372,100,140955,1,0
292,72,141221,6,0,B|250:102|244:152,2,85,0|8|0
332,148,142017,1,4
388,212,142283,2,0,B|414:243|465:241,1,85
440,148,142814,2,0,B|400:172|388:213,1,85
236,232,143345,1,0
204,84,143610,1,0
356,64,143876,1,0
388,212,144141,2,0,B|350:295|228:308,1,170,4|0
96,304,145203,6,0,B|96:208,1,85
144,203,145734,2,0,B|144:288,1,85,8|0
192,272,146265,2,0,B|192:176|192:176|192:120|256:112,1,170,4|0
312,56,147062,1,0
392,120,147327,6,0,B|392:208,1,85
336,221,147858,2,0,B|336:136,1,85,8|0
280,152,148389,2,0,B|280:256|280:256|264:272|280:288|280:288|296:304|280:320|280:320|248:336|280:352|280:352|312:368|312:368|280:376|224:384,1,340,4|4
172,322,149717,5,0
136,248,149982,1,8
64,208,150247,1,0
147,112,150513,5,0
224,80,150778,1,0
304,112,151044,1,8
384,88,151309,1,0
336,192,151575,6,0,B|280:272|176:264,1,170,0|8
408,216,152637,2,0,B|429:173|464:152,1,85,0|0
360,80,153168,2,0,B|376:168|304:264,1,170,8|0
256,288,153964,5,2
192,240,154230,1,4
272,208,154495,1,0
229,134,154761,2,0,B|276:214,1,85,0|2
160,248,155292,1,4
120,136,155557,1,0
229,134,155823,6,0,B|331:134,1,85,0|2
408,208,156354,2,0,B|312:208,1,85,4|0
216,256,156885,2,0,B|272:280|264:352|208:344|192:296|256:272|328:312,1,170,0|4
456,224,157947,5,0
400,136,158212,1,0
456,224,158478,1,8
392,304,158743,1,0
456,224,159009,1,0
288,232,159540,5,8
200,283,159805,1,0
176,184,160071,1,0
176,184,160601,5,8
278,184,160867,1,0
176,184,161132,2,0,B|88:184,1,85
24,88,161663,2,0,B|192:88,1,170,8|0
280,88,162460,1,2
240,168,162725,1,4
360,48,163256,5,0
280,88,163522,1,2
240,168,163787,2,0,B|344:168,1,85,4|0
376,240,164318,2,0,B|320:312,1,85,2|0
248,304,164849,2,0,B|200:232,1,85,6|0
288,240,165380,2,0,B|288:136|288:136|286:82|344:72,1,170,6|8
480,104,166442,6,0,B|416:168|416:296,1,170,4|8
336,280,167239,1,0
288,208,167504,2,0,B|232:288|120:264,1,170,0|8
56,184,168301,1,0
184,144,168566,1,8
96,248,168832,1,0
104,112,169097,1,8
176,232,169363,1,0
124,182,169628,1,8
272,256,170159,5,8
272,256,170424,1,0
272,256,170690,2,0,B|310:339|428:329,1,170,0|8
487,259,171486,1,0
423,179,171752,2,0,B|340:241|340:329,1,170,0|8
251,346,172548,1,0
260,193,172814,5,8
340,321,173079,1,0
260,193,173345,1,8
404,249,173610,1,0
260,193,173876,1,8
112,120,174407,6,0,B|117:173|144:200,1,85,8|0
309,191,174938,2,0,B|225:225|117:191,1,170,0|8
184,128,175734,1,0
264,104,176000,2,0,B|328:168|432:152,1,170,0|8
312,224,176796,1,0
240,339,177062,5,8
361,276,177327,1,0
245,204,177593,1,8
308,322,177858,1,0
225,270,178124,1,8
225,270,178655,6,0,B|176:256|144:208,1,85,8|0
32,256,179186,2,0,B|120:256|192:312,1,170,0|8
272,288,179982,1,0
352,248,180247,2,0,B|296:176|192:216,1,170,0|8
240,136,181044,1,0
325,129,181309,6,0,B|322:176|285:217,1,85,8|0
167,291,181840,2,0,B|170:244|207:203,1,85,8|0
327,289,182371,2,0,B|280:286|239:249,1,85,8|0
160,120,182902,2,0,B|216:112|248:152|272:192|336:192,1,170,8|4
256,192,183699,12,4,185557
80,104,202017,5,2
152,219,202283,1,0
16,224,202548,2,0,B|88:208|158:111,1,170,8|0
226,87,203345,1,0
304,120,203610,2,0,B|352:120|400:104,1,85,2|0
304,120,204141,2,0,B|336:88|344:32,1,85,0|0
341,45,204672,6,0,B|429:77|450:203,1,170,8|0
360,184,205469,1,0
304,120,205734,2,0,B|264:96|240:48,1,85,2|0
304,120,206265,2,0,B|311:76|344:32,1,85,0|0
408,88,206796,5,4
472,168,207062,1,0
392,224,207327,1,0
304,280,207593,1,0
224,208,207858,2,0,B|309:237|393:224,1,170
472,168,208655,1,0
408,88,208920,6,0,B|368:166|402:252,1,170,8|0
504,280,209717,1,0
403,319,209982,2,0,B|459:276|475:151,1,170,4|0
408,88,210778,1,0
384,200,211044,5,2
240,160,211309,1,0
264,304,211575,1,0
296,224,211840,2,0,B|336:137|464:136,1,170,2|0
296,224,212637,6,0,B|243:220|208:161,1,85,2|0
163,324,213168,2,0,B|244:308|308:204,1,170,8|0
296,136,213964,1,0
264,56,214230,2,0,B|232:96|192:136,1,85,4|0
208,120,214761,2,0,B|200:72|168:32,1,85
175,42,215292,2,0,B|155:86|98:112,1,85,2|0
50,53,215823,2,0,B|98:69|122:109,1,85,0|0
117,102,216354,1,4
168,344,216885,6,0,B|167:287|131:246,1,85
88,160,217416,2,0,B|48:248|96:328,1,170,8|0
144,264,218212,1,0
224,296,218478,2,0,B|328:312|368:216,1,170,6|0
363,110,219274,2,0,B|259:246|139:206|147:94|275:70|355:198|130:268,1,446.249986700714,2|8
160,112,221663,6,0,B|80:168|96:296,1,170,4|8
176,280,222460,1,0
224,208,222725,2,0,B|280:288|392:264,1,170,0|8
456,184,223522,1,0
328,144,223787,5,8
416,248,224053,1,0
408,112,224318,1,8
336,232,224584,1,0
388,182,224849,1,8
240,256,225380,5,8
240,256,225646,1,0
240,256,225911,2,0,B|184:328|76:314,1,170,0|8
3,315,226708,1,0
89,315,226973,2,0,B|184:302|240:374,1,170,0|8
314,332,227770,1,0
252,194,228035,5,8
116,130,228301,1,0
252,194,228566,1,8
140,298,228832,1,0
252,194,229097,1,8
400,120,229628,6,0,B|352:112|288:144,1,85,8|0
203,191,230159,2,0,B|287:225|395:191,1,170,0|8
330,124,230955,1,0
248,104,231221,2,0,B|152:96|80:152,1,170,0|8
200,224,232017,1,0
272,339,232283,5,8
151,276,232548,1,0
267,204,232814,1,8
204,322,233079,1,0
287,270,233345,1,8
287,270,233876,6,0,B|335:254|367:206,1,85,8|0
464,288,234407,2,0,B|368:272|304:344,1,170,0|8
226,317,235203,1,0
165,256,235469,2,0,B|224:192|336:208,1,170,0|8
272,136,236265,1,0
199,63,236531,2,0,B|152:80|120:128,1,85,8|0
203,184,237062,2,0,B|167:218|165:267,1,85,8|0
312,264,237593,5,8
440,264,237858,1,8
256,144,238124,1,8
496,144,238389,1,0
256,192,238655,12,4,240778
File diff suppressed because one or more lines are too long
@@ -0,0 +1,297 @@
osu file format v14
[General]
StackLeniency: 0.7
Mode: 0
[Difficulty]
HPDrainRate:5
CircleSize:4
OverallDifficulty:7
ApproachRate:7.5
SliderMultiplier:1.4
SliderTickRate:1
[Events]
//Background and Video events
//Break Periods
//Storyboard Layer 0 (Background)
//Storyboard Layer 1 (Fail)
//Storyboard Layer 2 (Pass)
//Storyboard Layer 3 (Foreground)
//Storyboard Sound Samples
[TimingPoints]
1107,365.853658536585,4,2,1,50,1,0
1107,-166.666666666667,4,2,1,50,0,0
6960,-111.111111111111,4,2,1,50,0,0
8424,-100,4,2,1,50,0,0
48119,-125,4,2,1,50,0,0
52143,-100,4,2,1,50,0,0
62570,-100,4,2,1,60,0,1
85985,-100,4,2,1,50,0,0
97692,-100,4,2,1,30,0,0
99155,-100,4,2,1,20,0,0
100619,-100,4,2,1,5,0,0
[HitObjects]
38,247,1107,6,0,P|96:269|170:192,1,167.999994873047,2|0,0:0|0:0,0:0:0:0:
201,128,2570,6,0,L|205:221,1,83.9999974365235,2|0,0:0|0:0,0:0:0:0:
242,230,3302,2,0,L|234:324,1,83.9999974365235,2|0,0:0|0:0,0:0:0:0:
205,343,4033,6,0,P|246:296|351:314,1,167.999994873047,2|0,0:0|0:0,0:0:0:0:
400,368,5497,6,0,L|412:269,1,83.9999974365235,6|0,0:0|0:0,0:0:0:0:
436,251,6228,2,0,P|425:203|408:153,1,83.9999974365235,2|0,0:0|0:0,0:0:0:0:
304,200,6960,6,0,P|262:186|234:181,1,62.9999980773926,6|0,0:0|0:0,0:0:0:0:
202,179,7326,1,8,0:0:0:0:
276,94,7509,2,0,P|313:92|353:87,1,62.9999980773926,2|0,0:0|0:0,0:0:0:0:
398,31,7875,1,2,0:0:0:0:
464,81,8058,2,0,L|450:150,1,62.9999980773926,2|0,0:0|0:0,0:0:0:0:
449,230,8424,6,0,P|347:206|306:217,1,140,2|8,0:0|0:0,0:0:0:0:
229,273,8972,2,0,P|225:339|235:361,1,70,2|0,0:0|0:0,0:0:0:0:
304,313,9338,1,8,0:0:0:0:
224,190,9521,1,2,0:0:0:0:
296,45,9887,6,0,P|297:97|288:125,1,70,6|0,0:0|0:0,0:0:0:0:
224,190,10253,1,8,0:0:0:0:
167,118,10436,1,8,0:0:0:0:
76,126,10619,1,8,0:0:0:0:
39,209,10802,1,8,0:0:0:0:
93,282,10985,1,10,0:0:0:0:
184,280,11167,1,10,0:0:0:0:
102,136,12814,5,2,0:0:0:0:
102,136,13180,2,0,L|199:130,1,70,8|0,0:0|0:0,0:0:0:0:
256,167,13546,2,0,L|339:161,1,70,8|2,0:0|0:0,0:0:0:0:
408,201,13911,2,0,P|454:176|471:143,1,70,8|2,0:0|0:0,0:0:0:0:
373,54,14277,6,0,L|396:137,2,70,6|0|8,0:0|0:0|0:0,0:0:0:0:
305,111,14826,2,0,L|287:274,1,140,0|2,0:0|0:0,0:0:0:0:
262,337,15375,2,0,L|349:327,1,70,8|2,0:0|0:0,0:0:0:0:
419,354,15741,1,8,0:0:0:0:
477,197,16106,6,0,P|423:197|385:209,1,70,8|0,0:0|0:0,0:0:0:0:
321,170,16472,2,0,P|278:190|253:219,1,70,8|2,0:0|0:0,0:0:0:0:
171,213,16838,2,0,P|152:259|158:304,1,70,8|2,0:0|0:0,0:0:0:0:
305,294,17204,6,0,L|224:278,2,70,6|0|8,0:0|0:0|0:0,0:0:0:0:
310,202,17753,2,0,L|149:214,1,140,0|2,0:0|0:0,0:0:0:0:
84,244,18302,2,0,L|92:152,1,70,8|2,0:0|0:0,0:0:0:0:
47,93,18667,6,0,P|78:53|176:80,1,140,6|8,0:0|0:0,0:0:0:0:
218,130,19216,1,0,0:0:0:0:
299,88,19399,2,0,L|387:91,1,70,8|0,0:0|0:0,0:0:0:0:
458,106,19765,2,0,P|447:139|444:205,1,70,8|0,0:0|0:0,0:0:0:0:
455,274,20131,5,2,0:0:0:0:
366,292,20314,2,0,L|353:211,1,70,0|8,0:0|0:0,0:0:0:0:
277,173,20680,2,0,L|253:342,1,140,0|2,0:0|0:0,0:0:0:0:
322,376,21228,2,0,P|368:368|416:370,1,70,8|2,0:0|0:0,0:0:0:0:
500,287,21594,6,0,P|427:273|362:293,2,140,6|8|8,0:0|0:0|0:0,0:0:0:0:
496,111,22509,1,8,0:0:0:0:
499,189,22692,2,0,L|418:191,1,70,8|2,0:0|0:0,0:0:0:0:
344,164,23058,5,6,0:0:0:0:
344,164,23241,1,12,0:0:0:0:
261,326,23606,2,0,L|246:178,1,140,8|2,0:0|0:0,0:0:0:0:
277,100,24155,2,0,P|225:99|196:109,1,70,8|2,0:0|0:0,0:0:0:0:
165,273,24521,5,6,0:0:0:0:
83,235,24704,2,0,L|93:81,1,140,0|0,0:0|0:0,0:0:0:0:
21,37,25253,2,0,L|1:120,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0:
110,17,25802,1,0,0:0:0:0:
172,83,25985,5,2,0:0:0:0:
236,19,26167,2,0,P|223:70|227:170,1,140,0|0,0:0|0:0,0:0:0:0:
293,216,26716,2,0,P|316:165|314:134,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0:
206,245,27265,1,0,0:0:0:0:
274,305,27448,5,2,0:0:0:0:
194,348,27631,2,0,L|363:332,1,140,0|0,0:0|0:0,0:0:0:0:
424,336,28180,1,2,0:0:0:0:
431,245,28363,2,0,P|381:252|354:276,2,70,0|8|0,0:0|0:0|0:0,0:0:0:0:
509,291,28911,6,0,L|496:128,1,140,2|8,0:0|0:0,0:0:0:0:
504,60,29460,1,0,0:0:0:0:
417,34,29643,2,0,L|402:183,1,140,2|8,0:0|0:0,0:0:0:0:
365,262,30192,1,0,0:0:0:0:
295,202,30375,5,2,0:0:0:0:
309,112,30558,2,0,P|282:172|196:176,1,140,0|0,0:0|0:0,0:0:0:0:
148,120,31106,2,0,P|189:99|225:99,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0:
129,209,31655,1,0,0:0:0:0:
63,146,31838,5,2,0:0:0:0:
16,67,32021,2,0,L|27:220,1,140,0|0,0:0|0:0,0:0:0:0:
23,297,32570,2,0,P|81:286|111:290,1,70,2|0,0:0|0:0,0:0:0:0:
173,327,32936,1,8,0:0:0:0:
338,251,33302,6,0,P|268:254|227:199,1,140,2|8,0:0|0:0,0:0:0:0:
203,114,33850,2,0,L|185:262,1,140,0|0,0:0|0:0,0:0:0:0:
244,323,34399,1,8,0:0:0:0:
334,335,34582,1,0,0:0:0:0:
419,219,34765,6,0,L|410:304,1,70,2|0,0:0|0:0,0:0:0:0:
338,251,35131,1,8,0:0:0:0:
301,111,35314,2,0,L|301:190,1,70,6|0,0:0|0:0,0:0:0:0:
383,141,35680,1,8,0:0:0:0:
462,97,35863,2,0,P|427:64|393:54,1,70,2|0,0:0|0:0,0:0:0:0:
321,23,36228,5,2,0:0:0:0:
237,60,36411,1,0,0:0:0:0:
148,38,36594,2,0,P|107:33|56:43,1,70,8|0,0:0|0:0,0:0:0:0:
86,125,36960,2,0,P|51:125|17:117,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0:
175,123,37509,1,0,0:0:0:0:
129,201,37692,5,2,0:0:0:0:
198,259,37875,1,0,0:0:0:0:
205,349,38058,2,0,P|251:330|284:326,1,70,8|0,0:0|0:0,0:0:0:0:
352,285,38424,2,0,P|361:318|357:353,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0:
282,239,38972,1,0,0:0:0:0:
362,195,39155,5,2,0:0:0:0:
436,142,39338,2,0,P|398:115|354:112,1,70,0|8,0:0|0:0,0:0:0:0:
286,92,39704,2,0,L|451:74,1,140,0|0,0:0|0:0,0:0:0:0:
512,118,40253,2,0,L|494:198,1,70,8|0,0:0|0:0,0:0:0:0:
430,297,40619,6,0,P|423:236|336:195,1,140,2|8,0:0|0:0,0:0:0:0:
282,239,41167,1,0,0:0:0:0:
209,184,41350,2,0,L|222:112,1,70,2|2,0:0|0:0,0:0:0:0:
177,34,41716,2,0,P|230:26|269:38,1,70,8|0,0:0|0:0,0:0:0:0:
307,95,42082,5,2,0:0:0:0:
363,23,42265,2,0,L|359:114,1,70,0|8,0:0|0:0,0:0:0:0:
360,184,42631,1,0,0:0:0:0:
450,191,42814,2,0,P|443:145|424:119,2,70,2|0|8,0:0|0:0|0:0,0:0:0:0:
393,263,43363,1,0,0:0:0:0:
304,242,43546,5,2,0:0:0:0:
241,308,43728,1,0,0:0:0:0:
167,256,43911,2,0,P|205:228|245:226,1,70,8|0,0:0|0:0,0:0:0:0:
166,341,44277,2,0,P|118:325|90:289,1,70,2|0,0:0|0:0,0:0:0:0:
125,177,44643,2,0,P|168:152|201:153,1,70,8|0,0:0|0:0,0:0:0:0:
276,132,45009,6,0,L|119:105,1,140,2|8,0:0|0:0,0:0:0:0:
52,74,45558,2,0,L|210:57,1,140,2|0,0:0|0:0,0:0:0:0:
277,28,46106,1,8,0:0:0:0:
349,82,46289,1,0,0:0:0:0:
425,32,46472,6,0,L|451:110,2,70,6|2|8,0:0|0:0|0:0,0:0:0:0:
349,82,47021,2,0,L|344:235,1,140,2|8,0:0|0:0,0:0:0:0:
372,308,47570,1,2,0:0:0:0:
170,324,47936,5,2,0:0:0:0:
99,286,48119,2,0,L|112:112,1,168,2|2,0:0|0:0,0:0:0:0:
64,48,48850,2,0,P|125:36|195:111,1,168,2|2,0:0|0:0,0:0:0:0:
199,189,49582,6,0,L|369:166,1,168,2|2,0:0|0:0,0:0:0:0:
413,97,50314,2,0,P|390:180|377:274,1,168,2|2,0:0|0:0,0:0:0:0:
347,339,51046,6,0,P|424:333|463:251,1,168,2|2,0:0|0:0,0:0:0:0:
473,175,51777,2,0,L|477:105,1,56,2|2,0:0|0:0,0:0:0:0:
446,24,52143,6,0,P|363:22|308:82,1,140,12|2,0:0|0:0,0:0:0:0:
282,138,52692,1,8,0:0:0:0:
193,118,52875,2,0,L|213:281,1,140,2|8,0:0|0:0,0:0:0:0:
225,347,53424,2,0,P|268:328|286:301,1,70,2|0,0:0|0:0,0:0:0:0:
304,222,53789,5,2,0:0:0:0:
385,263,53972,1,0,0:0:0:0:
462,214,54155,2,0,P|421:185|383:179,1,70,8|0,0:0|0:0,0:0:0:0:
322,136,54521,2,0,P|360:105|400:93,1,70,2|0,0:0|0:0,0:0:0:0:
469,107,54887,2,0,L|483:24,1,70,8|0,0:0|0:0,0:0:0:0:
390,22,55253,6,0,L|223:30,1,140,2|8,0:0|0:0,0:0:0:0:
180,87,55802,1,0,0:0:0:0:
230,162,55985,2,0,L|391:154,1,140,2|8,0:0|0:0,0:0:0:0:
430,223,56533,1,0,0:0:0:0:
407,311,56716,6,0,P|356:347|285:307,1,140,2|8,0:0|0:0,0:0:0:0:
236,245,57265,1,0,0:0:0:0:
145,237,57448,2,0,L|162:316,1,70,2|0,0:0|0:0,0:0:0:0:
233,360,57814,6,0,P|185:349|142:350,1,70,8|0,0:0|0:0,0:0:0:0:
11,311,58180,2,0,P|64:302|104:306,1,70,2|0,0:0|0:0,0:0:0:0:
213,248,58546,2,0,P|162:237|130:237,1,70,8|0,0:0|0:0,0:0:0:0:
1,194,58911,2,0,P|47:183|74:185,1,70,2|0,0:0|0:0,0:0:0:0:
234,142,59277,2,0,P|175:129|152:128,1,70,8|0,0:0|0:0,0:0:0:0:
12,26,59643,6,0,P|66:38|71:140,1,140,2|8,0:0|0:0,0:0:0:0:
1,194,60192,1,0,0:0:0:0:
84,230,60375,1,2,0:0:0:0:
173,216,60558,1,8,0:0:0:0:
173,216,60649,1,8,0:0:0:0:
173,216,60741,1,8,0:0:0:0:
263,213,60924,1,2,0:0:0:0:
345,174,61106,6,0,P|320:144|286:130,1,70,2|0,0:0|0:0,0:0:0:0:
200,134,61472,1,8,0:0:0:0:
249,57,61655,2,0,L|263:12,2,35,12|8|8,0:0|0:0|0:0,0:0:0:0:
157,64,62021,2,0,L|153:13,2,35,12|8|8,0:0|0:0|0:0,0:0:0:0:
118,150,62387,1,2,0:0:0:0:
101,260,62570,6,0,P|207:236|257:243,1,140,2|8,0:0|0:0,0:0:0:0:
328,304,63119,1,0,0:0:0:0:
434,156,63302,2,0,P|373:157|329:217,1,140,2|8,0:0|0:0,0:0:0:0:
408,230,63850,1,2,0:0:0:0:
483,215,64033,5,6,0:0:0:0:
508,142,64216,1,0,0:0:0:0:
482,69,64399,1,8,0:0:0:0:
413,34,64582,2,0,P|336:30|256:49,1,140,0|2,0:0|0:0,0:0:0:0:
150,97,65131,2,0,P|190:97|243:107,1,70,8|2,0:0|0:0,0:0:0:0:
257,168,65497,6,0,L|225:323,1,140,2|8,0:0|0:0,0:0:0:0:
155,329,66046,1,0,0:0:0:0:
20,204,66228,2,0,P|92:202|133:271,1,140,8|8,0:0|0:0,0:0:0:0:
56,274,66777,1,2,0:0:0:0:
18,125,66960,6,0,L|93:119,1,70,6|0,0:0|0:0,0:0:0:0:
162,156,67326,1,8,0:0:0:0:
223,52,67509,2,0,L|227:219,1,140,0|2,0:0|0:0,0:0:0:0:
266,263,68058,2,0,P|300:229|308:199,1,70,8|2,0:0|0:0,0:0:0:0:
298,95,68424,6,0,L|458:75,1,140,6|8,0:0|0:0,0:0:0:0:
512,164,68972,2,0,L|358:154,1,140,0|2,0:0|0:0,0:0:0:0:
306,209,69521,1,8,0:0:0:0:
342,334,69704,6,0,P|361:289|369:244,1,70,2|6,0:0|0:0,0:0:0:0:
250,277,70070,2,0,P|223:228|219:186,1,70,0|8,0:0|0:0,0:0:0:0:
272,128,70436,1,0,0:0:0:0:
172,111,70619,2,0,L|343:97,1,140,8|8,0:0|0:0,0:0:0:0:
385,128,71167,1,2,0:0:0:0:
494,63,71350,6,0,L|413:54,1,70,6|0,0:0|0:0,0:0:0:0:
385,128,71716,2,0,L|475:140,1,70,8|0,0:0|0:0,0:0:0:0:
467,217,72082,2,0,L|386:208,1,70,8|2,0:0|0:0,0:0:0:0:
358,282,72448,2,0,L|448:294,1,70,8|2,0:0|0:0,0:0:0:0:
498,339,72814,5,12,0:0:0:0:
498,339,72997,1,12,0:0:0:0:
301,343,73363,1,8,0:0:0:0:
211,173,73728,2,0,L|221:216,2,35,2|2|8,0:0|0:0|0:0,0:0:0:0:
250,100,74094,1,2,0:0:0:0:
123,92,74277,6,0,P|129:156|129:236,1,140,2|8,0:0|0:0,0:0:0:0:
109,321,74826,1,0,0:0:0:0:
211,173,75009,2,0,P|266:165|333:237,1,140,8|8,0:0|0:0,0:0:0:0:
341,302,75558,1,2,0:0:0:0:
418,272,75741,5,6,0:0:0:0:
484,322,75924,1,0,0:0:0:0:
407,352,76106,1,8,0:0:0:0:
341,302,76289,2,0,L|364:147,1,140,0|2,0:0|0:0,0:0:0:0:
269,60,76838,2,0,P|315:69|349:94,1,70,8|0,0:0|0:0,0:0:0:0:
269,150,77204,6,0,P|228:160|114:139,1,140,2|8,0:0|0:0,0:0:0:0:
49,80,77753,1,0,0:0:0:0:
39,235,77936,2,0,P|103:222|160:277,1,140,8|8,0:0|0:0,0:0:0:0:
82,297,78485,1,2,0:0:0:0:
227,326,78667,6,0,L|233:241,1,70,4|0,0:0|0:0,0:0:0:0:
269,150,79033,1,8,0:0:0:0:
408,194,79216,2,0,P|359:172|271:187,1,140,0|2,0:0|0:0,0:0:0:0:
409,281,79765,2,0,P|447:272|478:250,1,70,8|2,0:0|0:0,0:0:0:0:
497,168,80131,6,0,L|481:332,1,140,6|8,0:0|0:0,0:0:0:0:
389,365,80680,2,0,L|376:198,1,140,0|2,0:0|0:0,0:0:0:0:
414,157,81228,1,8,0:0:0:0:
229,89,81411,6,0,P|304:91|338:167,1,140,2|0,0:0|0:0,0:0:0:0:
290,222,81960,1,8,0:0:0:0:
211,214,82143,1,8,0:0:0:0:
93,155,82326,2,0,P|137:143|172:150,1,70,2|2,0:0|0:0,0:0:0:0:
235,301,82692,2,0,P|177:296|141:279,1,70,8|2,0:0|0:0,0:0:0:0:
68,244,83058,6,0,L|72:328,1,70,6|0,0:0|0:0,0:0:0:0:
166,292,83424,2,0,L|157:372,1,70,8|0,0:0|0:0,0:0:0:0:
254,227,83789,2,0,L|258:310,1,70,8|2,0:0|0:0,0:0:0:0:
345,265,84155,2,0,L|336:349,1,70,8|0,0:0|0:0,0:0:0:0:
331,175,84521,5,2,0:0:0:0:
416,205,84704,1,2,0:0:0:0:
481,141,84887,1,8,0:0:0:0:
431,64,85070,2,0,L|444:26,2,35,8|8|2,0:0|0:0|0:0,0:0:0:0:
339,79,85436,2,0,L|341:39,2,35,8|8|8,0:0|0:0|0:0,0:0:0:0:
256,109,85802,1,2,0:0:0:0:
165,97,85985,6,0,P|167:150|164:187,1,70,2|0,0:0|0:0,0:0:0:0:
117,244,86350,2,0,P|163:241|204:235,1,70,8|0,0:0|0:0,0:0:0:0:
229,317,86716,2,0,P|273:305|300:294,1,70,8|2,0:0|0:0,0:0:0:0:
365,354,87082,2,0,P|404:334|430:310,1,70,8|0,0:0|0:0,0:0:0:0:
352,230,87448,6,0,L|271:216,2,70,6|0|8,0:0|0:0|0:0,0:0:0:0:
378,142,87997,2,0,L|222:144,1,140,0|2,0:0|0:0,0:0:0:0:
152,112,88546,2,0,L|166:214,1,70,8|2,0:0|0:0,0:0:0:0:
139,270,88911,5,8,0:0:0:0:
12,138,89277,2,0,L|29:55,1,70,8|0,0:0|0:0,0:0:0:0:
91,5,89643,2,0,L|104:97,1,70,8|2,0:0|0:0,0:0:0:0:
153,149,90009,2,0,L|175:78,1,70,8|0,0:0|0:0,0:0:0:0:
279,36,90375,6,0,L|357:27,2,70,6|0|8,0:0|0:0|0:0,0:0:0:0:
248,122,90924,2,0,L|398:125,1,140,0|2,0:0|0:0,0:0:0:0:
479,123,91472,2,0,P|468:170|445:195,1,70,8|2,0:0|0:0,0:0:0:0:
365,204,91838,6,0,P|414:220|409:320,1,140,6|8,0:0|0:0,0:0:0:0:
354,354,92387,1,0,0:0:0:0:
262,353,92570,2,0,L|271:273,1,70,8|2,0:0|0:0,0:0:0:0:
297,196,92936,2,0,P|243:198|216:215,1,70,8|0,0:0|0:0,0:0:0:0:
172,276,93302,5,6,0:0:0:0:
137,360,93485,2,0,L|127:265,1,70,0|8,0:0|0:0,0:0:0:0:
81,212,93850,2,0,P|93:138|118:67,1,140,0|2,0:0|0:0,0:0:0:0:
170,4,94399,2,0,P|195:37|204:74,1,70,8|2,0:0|0:0,0:0:0:0:
186,153,94765,6,0,L|340:139,1,140,6|8,0:0|0:0,0:0:0:0:
408,101,95314,1,2,0:0:0:0:
443,184,95497,1,6,0:0:0:0:
369,237,95680,2,0,L|300:224,2,70,8|8|2,0:0|0:0|0:0,0:0:0:0:
448,282,96228,5,12,0:0:0:0:
448,282,96411,1,12,0:0:0:0:
270,320,96777,1,8,0:0:0:0:
313,143,97143,1,8,0:0:0:0:
377,314,97509,1,8,0:0:0:0:
256,192,97692,12,0,100619,0:0:0:0:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,126 @@
osu file format v5
[General]
StackLeniency: 0.7
Mode: 0
[Difficulty]
HPDrainRate:2
CircleSize:5
OverallDifficulty:2
SliderMultiplier:1
SliderTickRate:2
[Events]
//Background and Video events
//Break Periods
//Storyboard Layer 0 (Background)
//Storyboard Layer 1 (Failing)
//Storyboard Layer 2 (Passing)
//Storyboard Layer 3 (Foreground)
//Storyboard Sound Samples
//Background Colour Transformations
3,100,163,162,255
[TimingPoints]
7460,466.735154027506,4,1,0,100
[HitObjects]
80,56,7693,1,0
120,96,8043,1,0
176,104,8393,1,0
216,104,8626,1,0
256,104,8860,1,0
296,168,9326,5,0
296,208,9560,1,0
296,248,9793,1,0
216,256,10260,1,0
176,256,10493,1,0
136,256,10727,1,0
136,136,11427,5,0
136,72,11777,1,0
192,72,12127,1,0
232,72,12360,1,0
272,72,12594,1,0
280,152,13060,5,0
280,192,13294,1,0
280,232,13527,1,0
360,240,13994,1,0
400,240,14227,1,0
440,240,14461,1,0
256,192,14927,12,0,16561
256,192,16794,12,0,18078
192,96,18661,6,0,B|312:96,1,100
288,176,19595,2,0,B|168:176,1,100
192,256,20528,2,0,B|312:256,1,100
304,176,21462,2,0,B|240:176|248:88,1,100
168,104,22395,5,0
128,104,22628,2,0,B|296:368,1,300
328,352,24262,5,0
368,352,24495,1,0
368,232,25195,1,0
368,192,25429,1,0
280,104,26129,5,0
240,104,26362,2,0,B|40:352,1,300
88,336,27996,5,0
128,336,28229,1,0
136,216,28929,1,0
136,176,29163,1,0
256,176,29863,5,0
312,176,30213,1,0
352,176,30446,2,0,B|360:264|360:280|360:272|272:272,1,150
208,232,31730,5,0
208,168,32080,1,0
208,104,32430,1,0
248,104,32663,1,0
248,104,32780,1,0
120,160,33597,5,0
120,216,33947,1,0
120,256,34180,2,0,B|352:256,1,225
344,216,35464,6,0,B|200:128,1,150
176,136,36397,2,0,B|176:288,1,150
296,288,37564,6,0,B|296:208,1,75
296,152,38264,2,0,B|296:104,2,25
248,32,39197,1,0
208,32,39431,1,0
168,32,39664,1,0
168,72,39898,2,0,B|168:136,4,50
104,128,41298,5,0
168,136,41648,1,0
208,184,41998,1,0
232,216,42231,1,0
344,248,42931,5,0
344,208,43165,1,0
344,168,43398,1,0
304,168,43631,1,0
264,168,43865,1,0
224,168,44098,1,0
184,168,44332,1,0
144,168,44565,1,0
104,176,44798,6,0,B|32:240|160:272,1,150
192,272,45732,2,0,B|280:272|320:200,1,150
320,160,46665,2,0,B|248:96|176:136,1,150
144,144,47599,2,0,B|48:168,1,75
112,256,48532,6,0,B|256:336,1,150
280,320,49466,2,0,B|416:240,1,150
408,200,50399,2,0,B|256:136,1,150
232,144,51333,2,0,B|80:208,1,150
56,216,52266,5,0
96,216,52499,1,0
152,216,52849,2,0,B|248:216,1,75
328,88,54133,5,0
328,88,54366,1,0
328,88,54600,1,0
248,88,55066,5,0
248,88,55300,1,0
248,88,55533,1,0
256,168,56000,6,0,B|184:168,1,50
144,168,56583,1,0
144,168,56700,1,0
104,168,56933,1,0
264,168,57867,5,0
264,168,58100,1,0
264,168,58334,1,0
344,168,58800,5,0
344,168,59034,1,0
344,168,59267,1,0
@@ -0,0 +1,168 @@
{
"Mappings": [
{
"RandomW": 2659373485,
"RandomX": 3579807591,
"RandomY": 273326509,
"RandomZ": 272969173,
"StartTime": 500.0,
"Objects": [
{
"StartTime": 500.0,
"EndTime": 2500.0,
"Column": 0
},
{
"StartTime": 1500.0,
"EndTime": 2500.0,
"Column": 1
}
]
},
{
"RandomW": 3083803045,
"RandomX": 273326509,
"RandomY": 272969173,
"RandomZ": 2659373485,
"StartTime": 3000.0,
"Objects": [
{
"StartTime": 3000.0,
"EndTime": 4000.0,
"Column": 2
}
]
},
{
"RandomW": 4073554232,
"RandomX": 272969173,
"RandomY": 2659373485,
"RandomZ": 3083803045,
"StartTime": 4500.0,
"Objects": [
{
"StartTime": 4500.0,
"EndTime": 5500.0,
"Column": 4
}
]
},
{
"RandomW": 3420401969,
"RandomX": 2659373485,
"RandomY": 3083803045,
"RandomZ": 4073554232,
"StartTime": 6000.0,
"Objects": [
{
"StartTime": 6000.0,
"EndTime": 6500.0,
"Column": 2
}
]
},
{
"RandomW": 1129881182,
"RandomX": 3083803045,
"RandomY": 4073554232,
"RandomZ": 3420401969,
"StartTime": 7000.0,
"Objects": [
{
"StartTime": 7000.0,
"EndTime": 8000.0,
"Column": 2
}
]
},
{
"RandomW": 315568458,
"RandomX": 3420401969,
"RandomY": 1129881182,
"RandomZ": 2358617505,
"StartTime": 8500.0,
"Objects": [
{
"StartTime": 8500.0,
"EndTime": 11000.0,
"Column": 0
}
]
},
{
"RandomW": 548134043,
"RandomX": 1129881182,
"RandomY": 2358617505,
"RandomZ": 315568458,
"StartTime": 11500.0,
"Objects": [
{
"StartTime": 11500.0,
"EndTime": 12000.0,
"Column": 1
}
]
},
{
"RandomW": 3979422122,
"RandomX": 548134043,
"RandomY": 2810584254,
"RandomZ": 2250186050,
"StartTime": 12500.0,
"Objects": [
{
"StartTime": 12500.0,
"EndTime": 16500.0,
"Column": 4
}
]
},
{
"RandomW": 2466283411,
"RandomX": 2810584254,
"RandomY": 2250186050,
"RandomZ": 3979422122,
"StartTime": 17000.0,
"Objects": [
{
"StartTime": 17000.0,
"EndTime": 18000.0,
"Column": 2
}
]
},
{
"RandomW": 83157665,
"RandomX": 2250186050,
"RandomY": 3979422122,
"RandomZ": 2466283411,
"StartTime": 18500.0,
"Objects": [
{
"StartTime": 18500.0,
"EndTime": 19450.0,
"Column": 0
}
]
},
{
"RandomW": 2383087700,
"RandomX": 83157665,
"RandomY": 2055150192,
"RandomZ": 510071020,
"StartTime": 19875.0,
"Objects": [
{
"StartTime": 19875.0,
"EndTime": 23875.0,
"Column": 1
},
{
"StartTime": 19875.0,
"EndTime": 23875.0,
"Column": 0
}
]
}
]
}
@@ -1,27 +1,27 @@
osu file format v14
[Difficulty]
HPDrainRate:6
CircleSize:4
OverallDifficulty:7
ApproachRate:8.3
SliderMultiplier:1.6
SliderTickRate:1
[TimingPoints]
500,500,4,2,1,50,1,0
13426,-100,4,3,1,45,0,0
14884,-100,4,2,1,50,0,0
[HitObjects]
96,192,500,6,0,L|416:192,2,320
256,192,3000,12,0,4000,0:0:0:0:
256,192,4500,12,0,5500,0:0:0:0:
256,192,6000,12,0,6500,0:0:0:0:
256,128,7000,6,0,L|352:128,4,80
32,192,8500,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800
256,192,11500,12,0,12000,0:0:0:0:
512,320,12500,6,0,B|0:256|0:256|512:96|512:96|256:32,1,1280
256,256,17000,6,0,L|160:256,4,80
256,192,18500,12,0,19450,0:0:0:0:
216,231,19875,6,0,B|216:135|280:135|344:135|344:199|344:263|248:327|248:327|120:327|120:327|56:39|408:39|408:39|472:150|408:342,1,1280
osu file format v14
[Difficulty]
HPDrainRate:6
CircleSize:4
OverallDifficulty:7
ApproachRate:8.3
SliderMultiplier:1.6
SliderTickRate:1
[TimingPoints]
500,500,4,2,1,50,1,0
13426,-100,4,3,1,45,0,0
14884,-100,4,2,1,50,0,0
[HitObjects]
96,192,500,6,0,L|416:192,2,320
256,192,3000,12,0,4000,0:0:0:0:
256,192,4500,12,0,5500,0:0:0:0:
256,192,6000,12,0,6500,0:0:0:0:
256,128,7000,6,0,L|352:128,4,80
32,192,8500,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800
256,192,11500,12,0,12000,0:0:0:0:
512,320,12500,6,0,B|0:256|0:256|512:96|512:96|256:32,1,1280
256,256,17000,6,0,L|160:256,4,80
256,192,18500,12,0,19450,0:0:0:0:
216,231,19875,6,0,B|216:135|280:135|344:135|344:199|344:263|248:327|248:327|120:327|120:327|56:39|408:39|408:39|472:150|408:342,1,1280
@@ -0,0 +1,18 @@
{
"Mappings": [
{
"RandomW": 3083084786,
"RandomX": 273326509,
"RandomY": 273553282,
"RandomZ": 2659838971,
"StartTime": 4836.0,
"Objects": [
{
"StartTime": 4836.0,
"EndTime": 4836.0,
"Column": 0
}
]
}
]
}
@@ -200,12 +200,10 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
// judgement combo offset by perfect bonus judgement. see logic in DrawableNote.CheckForResult.
assertComboAtJudgement(1, 1);
assertComboAtJudgement(0, 1);
assertTailJudgement(HitResult.Meh);
assertComboAtJudgement(2, 0);
// judgement combo offset by perfect bonus judgement. see logic in DrawableNote.CheckForResult.
assertComboAtJudgement(4, 1);
assertComboAtJudgement(1, 0);
assertComboAtJudgement(3, 1);
}
/// <summary>
@@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.Tests
AddAssert("all objects perfectly judged",
() => judgementResults.Select(result => result.Type),
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_030));
AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
}
[Test]
@@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Mania.Tests
AddAssert("all objects perfectly judged",
() => judgementResults.Select(result => result.Type),
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_040));
AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
}
private void performTest(List<ManiaHitObject> hitObjects, List<ReplayFrame> frames)
@@ -57,31 +57,32 @@ 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 (difficulty.SourceRuleset.ShortName == ManiaRuleset.SHORT_NAME)
return (int)Math.Max(1, roundedCircleSize);
double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty);
int countSliderOrSpinner = difficulty.TotalObjectCount - difficulty.CircleCount;
float percentSpecialObjects = (float)countSliderOrSpinner / difficulty.TotalObjectCount;
if (difficulty.TotalObjectCount > 0 && difficulty.EndTimeObjectCount >= 0)
{
int countSliderOrSpinner = difficulty.EndTimeObjectCount;
if (percentSpecialObjects < 0.2)
return 7;
if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5)
return roundedOverallDifficulty > 5 ? 7 : 6;
if (percentSpecialObjects > 0.6)
return roundedOverallDifficulty > 4 ? 5 : 4;
// 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;
if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5)
return roundedOverallDifficulty > 5 ? 7 : 6;
if (percentSpecialObjects > 0.6)
return roundedOverallDifficulty > 4 ? 5 : 4;
}
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)
@@ -15,7 +15,11 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap)
{
return new LegacyScoreAttributes { ComboScore = 1000000 };
return new LegacyScoreAttributes
{
ComboScore = 1000000,
MaxCombo = 0 // Max combo is mod-dependent, so any value here is insufficient.
};
}
public double GetLegacyScoreMultiplier(IReadOnlyList<Mod> mods, LegacyBeatmapConversionDifficultyInfo difficulty)
@@ -5,6 +5,7 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
[Resolved]
private IScrollingInfo scrollingInfo { get; set; } = null!;
protected override bool IsValidForPlacement => HitObject.Duration > 0;
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
public HoldNotePlacementBlueprint()
: base(new HoldNote())
@@ -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)
+3
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
@@ -10,5 +10,10 @@ namespace osu.Game.Rulesets.Mania.Mods
public class ManiaModDoubleTime : ModDoubleTime, IManiaRateAdjustmentMod
{
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
// For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
// make the map harder and is more of a personal preference.
// In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.
public override double ScoreMultiplier => 1;
}
}
@@ -11,5 +11,10 @@ namespace osu.Game.Rulesets.Mania.Mods
public class ManiaModNightcore : ModNightcore<ManiaHitObject>, IManiaRateAdjustmentMod
{
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
// For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
// make the map any harder and is more of a personal preference.
// In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.
public override double ScoreMultiplier => 1;
}
}
@@ -1,11 +1,26 @@
// 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.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModPerfect : ModPerfect
{
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
{
if (!isRelevantResult(result.Judgement.MinResult) && !isRelevantResult(result.Judgement.MaxResult) && !isRelevantResult(result.Type))
return false;
// Mania allows imperfect "Great" hits without failing.
if (result.Judgement.MaxResult == HitResult.Perfect)
return result.Type < HitResult.Great;
return result.Type != result.Judgement.MaxResult;
}
private bool isRelevantResult(HitResult result) => result.AffectsAccuracy() || result.AffectsCombo();
}
}
@@ -13,8 +13,6 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
@@ -40,8 +38,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
private Drawable headPiece;
private DrawableNotePerfectBonus perfectBonus;
public DrawableNote()
: this(null)
{
@@ -93,10 +89,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (!userTriggered)
{
if (!HitObject.HitWindows.CanBeHit(timeOffset))
{
perfectBonus.TriggerResult(false);
ApplyResult(r => r.Type = r.Judgement.MinResult);
}
return;
}
@@ -107,16 +100,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
result = GetCappedResult(result);
perfectBonus.TriggerResult(result == HitResult.Perfect);
ApplyResult(r => r.Type = result);
}
public override void MissForcefully()
{
perfectBonus.TriggerResult(false);
base.MissForcefully();
}
/// <summary>
/// Some objects in mania may want to limit the max result.
/// </summary>
@@ -137,32 +123,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
switch (hitObject)
{
case DrawableNotePerfectBonus bonus:
AddInternal(perfectBonus = bonus);
break;
}
}
protected override void ClearNestedHitObjects()
{
RemoveInternal(perfectBonus, false);
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
{
switch (hitObject)
{
case NotePerfectBonus bonus:
return new DrawableNotePerfectBonus(bonus);
}
return base.CreateNestedHitObject(hitObject);
}
private void updateSnapColour()
{
if (beatmap == null || HitObject == null) return;
@@ -1,26 +0,0 @@
// 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.
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
public partial class DrawableNotePerfectBonus : DrawableManiaHitObject<NotePerfectBonus>
{
public override bool DisplayResult => false;
public DrawableNotePerfectBonus()
: this(null!)
{
}
public DrawableNotePerfectBonus(NotePerfectBonus hitObject)
: base(hitObject)
{
}
/// <summary>
/// Apply a judgement result.
/// </summary>
/// <param name="hit">Whether this tick was reached.</param>
internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
}
-8
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.Threading;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
@@ -13,12 +12,5 @@ namespace osu.Game.Rulesets.Mania.Objects
public class Note : ManiaHitObject
{
public override Judgement CreateJudgement() => new ManiaJudgement();
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
base.CreateNestedHitObjects(cancellationToken);
AddNested(new NotePerfectBonus { StartTime = StartTime });
}
}
}
@@ -1,20 +0,0 @@
// 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.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects
{
public class NotePerfectBonus : ManiaHitObject
{
public override Judgement CreateJudgement() => new NotePerfectBonusJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public class NotePerfectBonusJudgement : ManiaJudgement
{
public override HitResult MaxResult => HitResult.SmallBonus;
}
}
}
@@ -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)
@@ -1,132 +0,0 @@
{
"Mappings": [{
"RandomW": 2659373485,
"RandomX": 3579807591,
"RandomY": 273326509,
"RandomZ": 272969173,
"StartTime": 500.0,
"Objects": [{
"StartTime": 500.0,
"EndTime": 2500.0,
"Column": 0
}, {
"StartTime": 1500.0,
"EndTime": 2500.0,
"Column": 1
}]
}, {
"RandomW": 3083803045,
"RandomX": 273326509,
"RandomY": 272969173,
"RandomZ": 2659373485,
"StartTime": 3000.0,
"Objects": [{
"StartTime": 3000.0,
"EndTime": 4000.0,
"Column": 2
}]
}, {
"RandomW": 4073554232,
"RandomX": 272969173,
"RandomY": 2659373485,
"RandomZ": 3083803045,
"StartTime": 4500.0,
"Objects": [{
"StartTime": 4500.0,
"EndTime": 5500.0,
"Column": 4
}]
}, {
"RandomW": 3420401969,
"RandomX": 2659373485,
"RandomY": 3083803045,
"RandomZ": 4073554232,
"StartTime": 6000.0,
"Objects": [{
"StartTime": 6000.0,
"EndTime": 6500.0,
"Column": 2
}]
}, {
"RandomW": 1129881182,
"RandomX": 3083803045,
"RandomY": 4073554232,
"RandomZ": 3420401969,
"StartTime": 7000.0,
"Objects": [{
"StartTime": 7000.0,
"EndTime": 8000.0,
"Column": 2
}]
}, {
"RandomW": 315568458,
"RandomX": 3420401969,
"RandomY": 1129881182,
"RandomZ": 2358617505,
"StartTime": 8500.0,
"Objects": [{
"StartTime": 8500.0,
"EndTime": 11000.0,
"Column": 0
}]
}, {
"RandomW": 548134043,
"RandomX": 1129881182,
"RandomY": 2358617505,
"RandomZ": 315568458,
"StartTime": 11500.0,
"Objects": [{
"StartTime": 11500.0,
"EndTime": 12000.0,
"Column": 1
}]
}, {
"RandomW": 3979422122,
"RandomX": 548134043,
"RandomY": 2810584254,
"RandomZ": 2250186050,
"StartTime": 12500.0,
"Objects": [{
"StartTime": 12500.0,
"EndTime": 16500.0,
"Column": 4
}]
}, {
"RandomW": 2466283411,
"RandomX": 2810584254,
"RandomY": 2250186050,
"RandomZ": 3979422122,
"StartTime": 17000.0,
"Objects": [{
"StartTime": 17000.0,
"EndTime": 18000.0,
"Column": 2
}]
}, {
"RandomW": 83157665,
"RandomX": 2250186050,
"RandomY": 3979422122,
"RandomZ": 2466283411,
"StartTime": 18500.0,
"Objects": [{
"StartTime": 18500.0,
"EndTime": 19450.0,
"Column": 0
}]
}, {
"RandomW": 2383087700,
"RandomX": 83157665,
"RandomY": 2055150192,
"RandomZ": 510071020,
"StartTime": 19875.0,
"Objects": [{
"StartTime": 19875.0,
"EndTime": 23875.0,
"Column": 1
}, {
"StartTime": 19875.0,
"EndTime": 23875.0,
"Column": 0
}]
}]
}
@@ -1,14 +0,0 @@
{
"Mappings": [{
"RandomW": 3083084786,
"RandomX": 273326509,
"RandomY": 273553282,
"RandomZ": 2659838971,
"StartTime": 4836,
"Objects": [{
"StartTime": 4836,
"EndTime": 4836,
"Column": 0
}]
}]
}
@@ -26,13 +26,37 @@ namespace osu.Game.Rulesets.Mania.Scoring
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{
return 10000 * comboProgress
+ 990000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
return 150000 * comboProgress
+ 850000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
+ bonusPortion;
}
protected override double GetComboScoreChange(JudgementResult result)
=> Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base));
{
return getBaseComboScoreForResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base));
}
public override int GetBaseScoreForResult(HitResult result)
{
switch (result)
{
case HitResult.Perfect:
return 305;
}
return base.GetBaseScoreForResult(result);
}
private int getBaseComboScoreForResult(HitResult result)
{
switch (result)
{
case HitResult.Perfect:
return 300;
}
return GetBaseScoreForResult(result);
}
private class JudgementOrderComparer : IComparer<HitObject>
{
@@ -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));
@@ -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)
@@ -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";
}
}
-1
View File
@@ -109,7 +109,6 @@ namespace osu.Game.Rulesets.Mania.UI
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
RegisterPool<Note, DrawableNote>(10, 50);
RegisterPool<NotePerfectBonus, DrawableNotePerfectBonus>(10, 50);
RegisterPool<HoldNote, DrawableHoldNote>(10, 50);
RegisterPool<HeadNote, DrawableHoldNoteHead>(10, 50);
RegisterPool<TailNote, DrawableHoldNoteTail>(10, 50);
+39 -8
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)
@@ -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);
}
@@ -1,17 +1,21 @@
// 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;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModPerfect : ModPerfectTestScene
public partial class TestSceneOsuModPerfect : ModFailConditionTestScene
{
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
@@ -50,5 +54,30 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
CreateHitObjectTest(new HitObjectTestData(spinner), shouldMiss);
}
[Test]
public void TestMissSliderTail() => CreateModTest(new ModTestData
{
Mod = new OsuModPerfect(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true),
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Slider
{
Position = new Vector2(256, 192),
StartTime = 1000,
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton),
new OsuReplayFrame(1001, new Vector2(256, 192)),
}
});
}
}
@@ -0,0 +1,53 @@
// 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;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModStrictTracking : OsuModTestScene
{
[Test]
public void TestSliderInput() => CreateModTest(new ModTestData
{
Mod = new OsuModStrictTracking(),
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Slider
{
StartTime = 1000,
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(0, 100))
}
}
}
}
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(0, new Vector2(), OsuAction.LeftButton),
new OsuReplayFrame(500, new Vector2(200, 0), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(200, 0)),
new OsuReplayFrame(1000, new Vector2(), OsuAction.LeftButton),
new OsuReplayFrame(1750, new Vector2(0, 100), OsuAction.LeftButton),
new OsuReplayFrame(1751, new Vector2(0, 100)),
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2
});
}
}
@@ -0,0 +1,77 @@
// 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;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModSuddenDeath : ModFailConditionTestScene
{
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
public TestSceneOsuModSuddenDeath()
: base(new OsuModSuddenDeath())
{
}
[Test]
public void TestMissTail() => CreateModTest(new ModTestData
{
Mod = new OsuModSuddenDeath(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Slider
{
Position = new Vector2(256, 192),
StartTime = 1000,
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton),
new OsuReplayFrame(1001, new Vector2(256, 192)),
}
});
[Test]
public void TestMissTick() => CreateModTest(new ModTestData
{
Mod = new OsuModSuddenDeath(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true),
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Slider
{
Position = new Vector2(256, 192),
StartTime = 1000,
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(200, 0), })
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton),
new OsuReplayFrame(1001, new Vector2(256, 192)),
}
});
}
}
@@ -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));
}
}
}
@@ -10,12 +10,12 @@ using osu.Game.Rulesets.Osu.Scoring;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class TestSceneOsuHealthProcessor
public class TestSceneOsuLegacyHealthProcessor
{
[Test]
public void TestNoBreak()
{
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
OsuLegacyHealthProcessor hp = new OsuLegacyHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
{
HitObjects =
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestSingleBreak()
{
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
OsuLegacyHealthProcessor hp = new OsuLegacyHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
{
HitObjects =
@@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestOverlappingBreak()
{
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
OsuLegacyHealthProcessor hp = new OsuLegacyHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
{
HitObjects =
@@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestSequentialBreak()
{
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
OsuLegacyHealthProcessor hp = new OsuLegacyHealthProcessor(-1000);
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
{
HitObjects =
@@ -0,0 +1,229 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public partial class TestSceneSliderEarlyHitJudgement : RateAdjustedBeatmapTestScene
{
private const double time_slider_start = 1000;
private const double time_slider_end = 3000;
private static readonly Vector2 slider_start_position = new Vector2(256 - slider_path_length / 2, 192);
private static readonly Vector2 slider_end_position = new Vector2(256 + slider_path_length / 2, 192);
private static readonly Vector2 offset_inside_follow = new Vector2(35, 0);
private static readonly Vector2 offset_outside_follow = offset_inside_follow * 2;
private ScoreAccessibleReplayPlayer currentPlayer = null!;
private const float slider_path_length = 200;
private readonly List<JudgementResult> judgementResults = new List<JudgementResult>();
[Test]
public void TestHitEarlyMoveIntoFollowRegion()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start - 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_start - 100, slider_start_position + offset_inside_follow, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end - 100, slider_end_position + offset_inside_follow, OsuAction.LeftButton),
});
assertHeadJudgement(HitResult.Meh);
assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.LargeTickHit);
assertSliderJudgement(HitResult.IgnoreHit);
}
[Test]
public void TestHitEarlyAndReleaseInFollowRegion()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start - 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_start - 100, slider_start_position + offset_inside_follow, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_start - 50, slider_start_position + offset_inside_follow),
new OsuReplayFrame(time_slider_end - 50, slider_end_position + offset_inside_follow, OsuAction.LeftButton),
});
assertHeadJudgement(HitResult.Meh);
assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.IgnoreMiss);
assertSliderJudgement(HitResult.IgnoreHit);
}
[Test]
public void TestHitEarlyAndRepressInFollowRegion()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start - 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_start - 100, slider_start_position + offset_inside_follow, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_start - 75, slider_start_position + offset_inside_follow),
new OsuReplayFrame(time_slider_start - 50, slider_start_position + offset_inside_follow, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end - 50, slider_end_position + offset_inside_follow, OsuAction.LeftButton),
});
assertHeadJudgement(HitResult.Meh);
assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.IgnoreMiss);
assertSliderJudgement(HitResult.IgnoreHit);
}
[Test]
public void TestHitEarlyMoveOutsideFollowRegion()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start - 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_start - 100, slider_start_position + offset_outside_follow, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end - 100, slider_end_position + offset_outside_follow, OsuAction.LeftButton),
});
assertHeadJudgement(HitResult.Meh);
assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.IgnoreMiss);
assertSliderJudgement(HitResult.IgnoreHit);
}
private void assertHeadJudgement(HitResult result)
{
AddAssert(
"check head result",
() => judgementResults.SingleOrDefault(r => r.HitObject is SliderHeadCircle)?.Type,
() => Is.EqualTo(result));
}
private void assertTickJudgement(HitResult result)
{
AddAssert(
"check tick result",
() => judgementResults.SingleOrDefault(r => r.HitObject is SliderTick)?.Type,
() => Is.EqualTo(result));
}
private void assertRepeatJudgement(HitResult result)
{
AddAssert(
"check tick result",
() => judgementResults.SingleOrDefault(r => r.HitObject is SliderRepeat)?.Type,
() => Is.EqualTo(result));
}
private void assertTailJudgement(HitResult result)
{
AddAssert(
"check tail result",
() => judgementResults.SingleOrDefault(r => r.HitObject is SliderTailCircle)?.Type,
() => Is.EqualTo(result));
}
private void assertSliderJudgement(HitResult result)
{
AddAssert(
"check slider result",
() => judgementResults.SingleOrDefault(r => r.HitObject is Slider)?.Type,
() => Is.EqualTo(result));
}
private Vector2 computePositionFromTime(double time)
{
Vector2 dist = slider_end_position - slider_start_position;
double t = (time - time_slider_start) / (time_slider_end - time_slider_start);
return slider_start_position + dist * (float)t;
}
private void performTest(List<ReplayFrame> frames, Action<Slider>? adjustSliderFunc = null, bool classic = false)
{
Slider slider = new Slider
{
StartTime = time_slider_start,
Position = new Vector2(256 - slider_path_length / 2, 192),
TickDistanceMultiplier = 3,
ClassicSliderBehaviour = classic,
Samples = new[]
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
},
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(slider_path_length, 0),
}, slider_path_length),
};
adjustSliderFunc?.Invoke(slider);
AddStep("load player", () =>
{
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
{
HitObjects = { slider },
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty
{
SliderMultiplier = 1,
SliderTickRate = 3,
OverallDifficulty = 0
},
Ruleset = new OsuRuleset().RulesetInfo,
}
});
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
{
p.ScoreProcessor.NewJudgement += result =>
{
if (currentPlayer == p) judgementResults.Add(result);
};
};
LoadScreen(currentPlayer = p);
judgementResults.Clear();
});
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
}
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleReplayPlayer(Score score)
: base(score, new PlayerConfiguration
{
AllowPause = false,
ShowResults = false,
})
{
}
}
}
}
@@ -0,0 +1,528 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public partial class TestSceneSliderLateHitJudgement : RateAdjustedBeatmapTestScene
{
// Note: In the following tests, the terminology "in range of the follow circle" is used as meaning
// the equivalent of "in range of the follow circle as if it were in its expanded state".
private const double time_slider_start = 1000;
private const double time_slider_end = 1500;
private static readonly Vector2 slider_start_position = new Vector2(256 - slider_path_length / 2, 192);
private static readonly Vector2 slider_end_position = new Vector2(256 + slider_path_length / 2, 192);
private ScoreAccessibleReplayPlayer currentPlayer = null!;
private const float slider_path_length = 200;
private readonly List<JudgementResult> judgementResults = new List<JudgementResult>();
/// <summary>
/// If the head circle is hit and the mouse is in range of the follow circle,
/// then tracking should be enabled.
/// </summary>
[Test]
public void TestHitLateInRangeTracks()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 100, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 100, slider_end_position, OsuAction.LeftButton),
});
assertHeadJudgement(HitResult.Ok);
assertTailJudgement(HitResult.LargeTickHit);
assertSliderJudgement(HitResult.IgnoreHit);
}
/// <summary>
/// If the head circle is hit and the mouse is NOT in range of the follow circle,
/// then tracking should NOT be enabled.
/// </summary>
[Test]
public void TestHitLateOutOfRangeDoesNotTrack()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 100, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 100, slider_end_position, OsuAction.LeftButton),
}, s =>
{
s.SliderVelocityMultiplier = 2;
});
assertHeadJudgement(HitResult.Ok);
assertTailJudgement(HitResult.IgnoreMiss);
assertSliderJudgement(HitResult.IgnoreHit);
}
/// <summary>
/// If the head circle is hit late and the mouse is in range of the follow circle,
/// then all ticks that the follow circle has passed through should be hit.
/// </summary>
[Test]
public void TestHitLateInRangeHitsTicks()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 150, slider_end_position, OsuAction.LeftButton),
}, s =>
{
s.TickDistanceMultiplier = 0.2f;
});
assertHeadJudgement(HitResult.Meh);
assertTickJudgement(0, HitResult.LargeTickHit);
assertTickJudgement(1, HitResult.LargeTickHit);
assertTickJudgement(2, HitResult.LargeTickHit);
assertTickJudgement(3, HitResult.LargeTickHit);
assertTailJudgement(HitResult.LargeTickHit);
assertSliderJudgement(HitResult.IgnoreHit);
}
/// <summary>
/// If the head circle is hit late and the mouse is NOT in range of the follow circle,
/// then all ticks that the follow circle has passed through should NOT be hit.
/// </summary>
[Test]
public void TestHitLateOutOfRangeDoesNotHitTicks()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 150, slider_end_position, OsuAction.LeftButton),
}, s =>
{
s.SliderVelocityMultiplier = 2;
s.TickDistanceMultiplier = 0.2f;
});
assertHeadJudgement(HitResult.Meh);
assertTickJudgement(0, HitResult.LargeTickMiss);
assertTickJudgement(1, HitResult.LargeTickMiss);
assertTailJudgement(HitResult.IgnoreMiss);
assertSliderJudgement(HitResult.IgnoreHit);
}
/// <summary>
/// If the head circle is pressed after it's missed and the mouse is in range of the follow circle,
/// then tracking should NOT be enabled.
/// </summary>
[Test]
public void TestMissHeadInRangeDoesNotTrack()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 151, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 151, slider_end_position, OsuAction.LeftButton),
}, s =>
{
s.TickDistanceMultiplier = 0.2f;
});
assertHeadJudgement(HitResult.Miss);
assertTickJudgement(0, HitResult.LargeTickMiss);
assertTickJudgement(1, HitResult.LargeTickMiss);
assertTickJudgement(2, HitResult.LargeTickMiss);
assertTickJudgement(3, HitResult.LargeTickMiss);
assertTailJudgement(HitResult.IgnoreMiss);
assertSliderJudgement(HitResult.IgnoreMiss);
}
/// <summary>
/// If the head circle is hit late but after the completion of the slider and the mouse is in range of the follow circle,
/// then all nested objects (ticks/repeats/tail) should be hit.
/// </summary>
[Test]
public void TestHitLateShortSliderHitsAll()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
}, s =>
{
s.Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(20, 0),
}, 20);
s.TickDistanceMultiplier = 0.01f;
s.RepeatCount = 1;
});
assertHeadJudgement(HitResult.Meh);
assertAllTickJudgements(HitResult.LargeTickHit);
assertRepeatJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.LargeTickHit);
assertSliderJudgement(HitResult.IgnoreHit);
}
/// <summary>
/// If the head circle is hit late and the mouse is in range of the follow circle,
/// then all the repeats that the follow circle has passed through should be hit.
/// </summary>
[Test]
public void TestHitLateInRangeHitsRepeat()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
}, s =>
{
s.Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(50, 0),
}, 50);
s.RepeatCount = 1;
});
assertHeadJudgement(HitResult.Meh);
assertRepeatJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.LargeTickHit);
assertSliderJudgement(HitResult.IgnoreHit);
}
/// <summary>
/// If the head circle is hit and the mouse is in range of the follow circle,
/// then only the ticks that are in range of the cursor position should be hit.
/// If any hitobject does not meet this criteria, ALL hitobjects after that one should be missed.
/// </summary>
[Test]
public void TestHitLateDoesNotHitTicksIfAnyOutOfRange()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
}, s =>
{
s.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(70, 70),
new Vector2(20, 0),
});
s.TickDistanceMultiplier = 0.03f;
s.SliderVelocityMultiplier = 6f;
});
assertHeadJudgement(HitResult.Meh);
// At least one tick was out of range, so they all should be missed.
assertAllTickJudgements(HitResult.LargeTickMiss);
// This particular test actually starts tracking the slider just before the end, so the tail should be hit because of its leniency.
assertTailJudgement(HitResult.LargeTickHit);
assertSliderJudgement(HitResult.IgnoreHit);
}
/// <summary>
/// If the head circle is hit and the mouse is in range of the follow circle,
/// then a tick not within the follow radius from the cursor position should not be hit.
/// </summary>
[Test]
public void TestHitLateInRangeDoesNotHitOutOfRangeTick()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
}, s =>
{
s.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(50, 50),
new Vector2(20, 0),
});
s.TickDistanceMultiplier = 0.3f;
s.SliderVelocityMultiplier = 3;
});
assertHeadJudgement(HitResult.Meh);
assertTickJudgement(0, HitResult.LargeTickMiss);
assertTailJudgement(HitResult.LargeTickHit);
assertSliderJudgement(HitResult.IgnoreHit);
}
/// <summary>
/// Same as <see cref="TestHitLateInRangeDoesNotHitOutOfRangeTick"/> except the tracking is limited to the ball
/// because the tick was missed.
/// </summary>
[Test]
public void TestHitLateInRangeDoesNotHitOutOfRangeTickAndTrackingLimitedToBall()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
}, s =>
{
s.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(50, 50),
new Vector2(20, 0),
});
s.TickDistanceMultiplier = 0.25f;
s.SliderVelocityMultiplier = 3;
});
assertHeadJudgement(HitResult.Meh);
assertTickJudgement(0, HitResult.LargeTickMiss);
assertTickJudgement(1, HitResult.LargeTickMiss);
assertTailJudgement(HitResult.LargeTickHit);
assertSliderJudgement(HitResult.IgnoreHit);
}
/// <summary>
/// If the head circle is hit and the mouse is in range of the follow circle,
/// then a tick not within the follow radius from the cursor position should not be hit.
/// </summary>
[Test]
public void TestHitLateWithEdgeHit()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame(time_slider_start + 150, slider_start_position - new Vector2(20), OsuAction.LeftButton),
new OsuReplayFrame(time_slider_end + 150, slider_start_position - new Vector2(20), OsuAction.LeftButton),
}, s =>
{
s.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(50, 50),
new Vector2(20, 0),
});
s.TickDistanceMultiplier = 0.35f;
s.SliderVelocityMultiplier = 4;
});
assertHeadJudgement(HitResult.Meh);
assertTickJudgement(0, HitResult.LargeTickMiss);
assertTailJudgement(HitResult.IgnoreMiss);
assertSliderJudgement(HitResult.IgnoreHit);
}
/// <summary>
/// Late hit and release on each slider head of a slider stream.
/// </summary>
[Test]
public void TestLateHitSliderStream()
{
var beatmap = new Beatmap<OsuHitObject>();
for (int i = 0; i < 20; i++)
{
beatmap.HitObjects.Add(new Slider
{
StartTime = time_slider_start + 75 * i, // 200BPM @ 1/4
Position = new Vector2(256 - slider_path_length / 2, 192),
TickDistanceMultiplier = 3,
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(20, 0),
}),
});
}
var replay = new List<ReplayFrame>();
for (int i = 0; i < 20; i++)
{
replay.Add(new OsuReplayFrame(time_slider_start + 75 * i + 75, slider_start_position, i % 2 == 0 ? OsuAction.LeftButton : OsuAction.RightButton));
replay.Add(new OsuReplayFrame(time_slider_start + 75 * i + 140, slider_start_position));
}
performTest(replay, beatmap);
AddAssert(
$"all heads = {HitResult.Ok}",
() => judgementResults.Where(r => r.HitObject is SliderHeadCircle).Select(r => r.Type),
() => Has.All.EqualTo(HitResult.Ok));
}
private void assertHeadJudgement(HitResult result)
{
AddAssert(
$"head = {result}",
() => judgementResults.SingleOrDefault(r => r.HitObject is SliderHeadCircle)?.Type,
() => Is.EqualTo(result));
}
private void assertTickJudgement(int index, HitResult result)
{
AddAssert(
$"tick({index}) = {result}",
() => judgementResults.Where(r => r.HitObject is SliderTick).ElementAtOrDefault(index)?.Type,
() => Is.EqualTo(result));
}
private void assertAllTickJudgements(HitResult result)
{
AddAssert(
$"all ticks = {result}",
() => judgementResults.Where(r => r.HitObject is SliderTick).Select(t => t.Type),
() => Has.All.EqualTo(result));
}
private void assertRepeatJudgement(HitResult result)
{
AddAssert(
$"repeat = {result}",
() => judgementResults.SingleOrDefault(r => r.HitObject is SliderRepeat)?.Type,
() => Is.EqualTo(result));
}
private void assertTailJudgement(HitResult result)
{
AddAssert(
$"tail = {result}",
() => judgementResults.SingleOrDefault(r => r.HitObject is SliderTailCircle)?.Type,
() => Is.EqualTo(result));
}
private void assertSliderJudgement(HitResult result)
{
AddAssert(
$"slider = {result}",
() => judgementResults.SingleOrDefault(r => r.HitObject is Slider)?.Type,
() => Is.EqualTo(result));
}
private void performTest(List<ReplayFrame> frames, Action<Slider>? adjustSliderFunc = null, bool classic = false)
{
Slider slider = new Slider
{
StartTime = time_slider_start,
Position = new Vector2(256 - slider_path_length / 2, 192),
TickDistanceMultiplier = 3,
ClassicSliderBehaviour = classic,
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(slider_path_length, 0),
}, slider_path_length),
};
adjustSliderFunc?.Invoke(slider);
var beatmap = new Beatmap<OsuHitObject>
{
HitObjects = { slider },
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty
{
SliderMultiplier = 4,
SliderTickRate = 3
},
Ruleset = new OsuRuleset().RulesetInfo,
}
};
performTest(frames, beatmap);
}
private void performTest(List<ReplayFrame> frames, Beatmap<OsuHitObject> beatmap)
{
beatmap.BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo;
beatmap.BeatmapInfo.StackLeniency = 0;
beatmap.BeatmapInfo.Difficulty = new BeatmapDifficulty
{
SliderMultiplier = 4,
SliderTickRate = 3,
};
AddStep("load player", () =>
{
Beatmap.Value = CreateWorkingBeatmap(beatmap);
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
{
p.ScoreProcessor.NewJudgement += result =>
{
if (currentPlayer == p)
judgementResults.Add(result);
DrawableHitObject drawableObj = this.ChildrenOfType<DrawableHitObject>().Single(h => h.HitObject == result.HitObject);
var text = new OsuSpriteText
{
Origin = Anchor.Centre,
Position = Content.ToLocalSpace(drawableObj.ToScreenSpace(drawableObj.OriginPosition)) - new Vector2(0, 20),
Text = result.IsHit ? "hit" : "miss"
};
Add(text);
text.FadeOutFromOne(1000).Expire();
};
};
LoadScreen(currentPlayer = p);
judgementResults.Clear();
});
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
}
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleReplayPlayer(Score score)
: base(score, new PlayerConfiguration
{
AllowPause = false,
ShowResults = false,
})
{
}
}
}
}
@@ -58,10 +58,7 @@ namespace osu.Game.Rulesets.Osu.Tests
double trackerRotationTolerance = 0;
addSeekStep(5000);
AddStep("calculate rotation tolerance", () =>
{
trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f);
});
AddStep("calculate rotation tolerance", () => { trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f); });
AddAssert("is disc rotation not almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.Not.EqualTo(0).Within(100));
AddAssert("is disc rotation absolute not almost 0", () => drawableSpinner.Result.TotalRotation, () => Is.Not.EqualTo(0).Within(100));
@@ -133,9 +130,11 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("player score matching expected bonus score", () =>
{
var scoreProcessor = ((ScoreExposedPlayer)Player).ScoreProcessor;
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
long totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
return totalScore == (int)(drawableSpinner.Result.TotalRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult;
long totalScore = scoreProcessor.TotalScore.Value * 2;
return totalScore == (int)(drawableSpinner.Result.TotalRotation / 360) * scoreProcessor.GetBaseScoreForResult(new SpinnerTick().CreateJudgement().MaxResult);
});
addSeekStep(0);
@@ -5,12 +5,12 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Scoring.Legacy;
@@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
internal class OsuLegacyScoreSimulator : ILegacyScoreSimulator
{
private readonly ScoreProcessor scoreProcessor = new OsuScoreProcessor();
private int legacyBonusScore;
private int standardisedBonusScore;
private int combo;
@@ -74,6 +76,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;
}
@@ -169,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (isBonus)
{
legacyBonusScore += scoreIncrease;
standardisedBonusScore += Judgement.ToNumericResult(bonusResult);
standardisedBonusScore += scoreProcessor.GetBaseScoreForResult(bonusResult);
}
else
attributes.AccuracyScore += scoreIncrease;
@@ -373,7 +373,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
curveTypeItems.Add(createMenuItemForPathType(PathType.LINEAR));
curveTypeItems.Add(createMenuItemForPathType(PathType.PERFECT_CURVE));
curveTypeItems.Add(createMenuItemForPathType(PathType.BEZIER));
curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(3)));
curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(4)));
if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull))
curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL));
@@ -3,6 +3,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@@ -41,15 +42,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private int currentSegmentLength;
[Resolved(CanBeNull = true)]
[CanBeNull]
private IPositionSnapProvider positionSnapProvider { get; set; }
[Resolved(CanBeNull = true)]
[CanBeNull]
private IDistanceSnapProvider distanceSnapProvider { get; set; }
[Resolved(CanBeNull = true)]
[CanBeNull]
private FreehandSliderToolboxGroup freehandToolboxGroup { get; set; }
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder();
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
@@ -94,6 +98,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
bSplineBuilder.CornerThreshold = e.NewValue;
Scheduler.AddOnce(updateSliderPathFromBSplineBuilder);
}, true);
freehandToolboxGroup.CircleThreshold.BindValueChanged(e =>
{
Scheduler.AddOnce(updateSliderPathFromBSplineBuilder);
}, true);
}
}
@@ -197,7 +206,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
base.OnDragEnd(e);
if (state == SliderPlacementState.Drawing)
{
bSplineBuilder.Finish();
updateSliderPathFromBSplineBuilder();
// Change the state so it will snap the expected distance in endCurve.
state = SliderPlacementState.Finishing;
endCurve();
}
}
protected override void OnMouseUp(MouseUpEvent e)
@@ -232,7 +248,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
if (state == SliderPlacementState.Drawing)
{
segmentStart.Type = PathType.BSpline(3);
segmentStart.Type = PathType.BSpline(4);
return;
}
@@ -300,7 +316,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updateSlider()
{
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
if (state == SliderPlacementState.Drawing)
HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance;
else
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
bodyPiece.UpdateFrom(HitObject);
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
@@ -309,53 +328,126 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updateSliderPathFromBSplineBuilder()
{
IReadOnlyList<Vector2> builderPoints = bSplineBuilder.ControlPoints;
IReadOnlyList<List<Vector2>> builderPoints = bSplineBuilder.ControlPoints;
if (builderPoints.Count == 0)
if (builderPoints.Count == 0 || builderPoints[0].Count == 0)
return;
int lastSegmentStart = 0;
PathType? lastPathType = null;
HitObject.Path.ControlPoints.Clear();
// Iterate through generated points, finding each segment and adding non-inheriting path types where appropriate.
// Importantly, the B-Spline builder returns three Vector2s at the same location when a new segment is to be started.
// Iterate through generated segments and adding non-inheriting path types where appropriate.
for (int i = 0; i < builderPoints.Count; i++)
{
bool isLastPoint = i == builderPoints.Count - 1;
bool isNewSegment = i < builderPoints.Count - 2 && builderPoints[i] == builderPoints[i + 1] && builderPoints[i] == builderPoints[i + 2];
bool isLastSegment = i == builderPoints.Count - 1;
var segment = builderPoints[i];
if (isNewSegment || isLastPoint)
if (segment.Count == 0)
continue;
// Replace this segment with a circular arc if it is a reasonable substitute.
var circleArcSegment = tryCircleArc(segment);
if (circleArcSegment is not null)
{
int pointsInSegment = i - lastSegmentStart;
// Where possible, we can use the simpler LINEAR path type.
PathType? pathType = pointsInSegment == 1 ? PathType.LINEAR : PathType.BSpline(3);
// Linear segments can be combined, as two adjacent linear sections are computationally the same as one with the points combined.
if (lastPathType == pathType && lastPathType == PathType.LINEAR)
pathType = null;
HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[lastSegmentStart], pathType));
for (int j = lastSegmentStart + 1; j < i; j++)
HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[j]));
if (isLastPoint)
HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[i]));
// Skip the redundant duplicated points (see isNewSegment above) which have been coalesced into a path type.
lastSegmentStart = (i += 2);
if (pathType != null) lastPathType = pathType;
HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[0], PathType.PERFECT_CURVE));
HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[1]));
}
else
{
HitObject.Path.ControlPoints.Add(new PathControlPoint(segment[0], PathType.BSpline(4)));
for (int j = 1; j < segment.Count - 1; j++)
HitObject.Path.ControlPoints.Add(new PathControlPoint(segment[j]));
}
if (isLastSegment)
HitObject.Path.ControlPoints.Add(new PathControlPoint(segment[^1]));
}
}
private Vector2[] tryCircleArc(List<Vector2> segment)
{
if (segment.Count < 3 || freehandToolboxGroup?.CircleThreshold.Value == 0) return null;
// Assume the segment creates a reasonable circular arc and then check if it reasonable
var points = PathApproximator.BSplineToPiecewiseLinear(segment.ToArray(), bSplineBuilder.Degree);
var circleArcControlPoints = new[] { points[0], points[points.Count / 2], points[^1] };
var circleArc = new CircularArcProperties(circleArcControlPoints);
if (!circleArc.IsValid) return null;
double length = circleArc.ThetaRange * circleArc.Radius;
if (length > 1000) return null;
double loss = 0;
Vector2? lastPoint = null;
Vector2? lastVec = null;
Vector2? lastVec2 = null;
int? lastDir = null;
int? lastDir2 = null;
double totalWinding = 0;
// Loop through the points and check if they are not too far away from the circular arc.
// Also make sure it curves monotonically in one direction and at most one loop is done.
foreach (var point in points)
{
var vec = point - circleArc.Centre;
loss += Math.Pow((vec.Length - circleArc.Radius) / length, 2);
if (lastVec.HasValue)
{
double det = lastVec.Value.X * vec.Y - lastVec.Value.Y * vec.X;
int dir = Math.Sign(det);
if (dir == 0)
continue;
if (lastDir.HasValue && dir != lastDir)
return null; // Circle center is not inside the polygon
lastDir = dir;
}
lastVec = vec;
if (lastPoint.HasValue)
{
var vec2 = point - lastPoint.Value;
if (lastVec2.HasValue)
{
double dot = Vector2.Dot(vec2, lastVec2.Value);
double det = lastVec2.Value.X * vec2.Y - lastVec2.Value.Y * vec2.X;
double angle = Math.Atan2(det, dot);
int dir2 = Math.Sign(angle);
if (dir2 == 0)
continue;
if (lastDir2.HasValue && dir2 != lastDir2)
return null; // Curvature changed, like in an S-shape
totalWinding += Math.Abs(angle);
lastDir2 = dir2;
}
lastVec2 = vec2;
}
lastPoint = point;
}
loss /= points.Count;
return loss > freehandToolboxGroup?.CircleThreshold.Value || totalWinding > MathHelper.TwoPi ? null : circleArcControlPoints;
}
private enum SliderPlacementState
{
Initial,
ControlPoints,
Drawing
Drawing,
Finishing
}
}
}
@@ -17,10 +17,10 @@ namespace osu.Game.Rulesets.Osu.Edit
{
}
public BindableFloat Tolerance { get; } = new BindableFloat(1.5f)
public BindableFloat Tolerance { get; } = new BindableFloat(1.8f)
{
MinValue = 0.05f,
MaxValue = 3f,
MaxValue = 2.0f,
Precision = 0.01f
};
@@ -31,8 +31,15 @@ namespace osu.Game.Rulesets.Osu.Edit
Precision = 0.01f
};
public BindableFloat CircleThreshold { get; } = new BindableFloat(0.0015f)
{
MinValue = 0f,
MaxValue = 0.005f,
Precision = 0.0001f
};
// We map internal ranges to a more standard range of values for display to the user.
private readonly BindableInt displayTolerance = new BindableInt(40)
private readonly BindableInt displayTolerance = new BindableInt(90)
{
MinValue = 5,
MaxValue = 100
@@ -44,8 +51,15 @@ namespace osu.Game.Rulesets.Osu.Edit
MaxValue = 100
};
private readonly BindableInt displayCircleThreshold = new BindableInt(30)
{
MinValue = 0,
MaxValue = 100
};
private ExpandableSlider<int> toleranceSlider = null!;
private ExpandableSlider<int> cornerThresholdSlider = null!;
private ExpandableSlider<int> circleThresholdSlider = null!;
[BackgroundDependencyLoader]
private void load()
@@ -59,6 +73,10 @@ namespace osu.Game.Rulesets.Osu.Edit
cornerThresholdSlider = new ExpandableSlider<int>
{
Current = displayCornerThreshold
},
circleThresholdSlider = new ExpandableSlider<int>
{
Current = displayCircleThreshold
}
};
}
@@ -83,18 +101,32 @@ namespace osu.Game.Rulesets.Osu.Edit
CornerThreshold.Value = displayToInternalCornerThreshold(threshold.NewValue);
}, true);
displayCircleThreshold.BindValueChanged(threshold =>
{
circleThresholdSlider.ContractedLabelText = $"P. C. T.: {threshold.NewValue:N0}";
circleThresholdSlider.ExpandedLabelText = $"Perfect Curve Threshold: {threshold.NewValue:N0}";
CircleThreshold.Value = displayToInternalCircleThreshold(threshold.NewValue);
}, true);
Tolerance.BindValueChanged(tolerance =>
displayTolerance.Value = internalToDisplayTolerance(tolerance.NewValue)
);
CornerThreshold.BindValueChanged(threshold =>
displayCornerThreshold.Value = internalToDisplayCornerThreshold(threshold.NewValue)
);
CircleThreshold.BindValueChanged(threshold =>
displayCircleThreshold.Value = internalToDisplayCircleThreshold(threshold.NewValue)
);
float displayToInternalTolerance(float v) => v / 33f;
int internalToDisplayTolerance(float v) => (int)Math.Round(v * 33f);
float displayToInternalTolerance(float v) => v / 50f;
int internalToDisplayTolerance(float v) => (int)Math.Round(v * 50f);
float displayToInternalCornerThreshold(float v) => v / 100f;
int internalToDisplayCornerThreshold(float v) => (int)Math.Round(v * 100f);
float displayToInternalCircleThreshold(float v) => v / 20000f;
int internalToDisplayCircleThreshold(float v) => (int)Math.Round(v * 20000f);
}
}
}
@@ -1,14 +1,11 @@
// 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.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAccuracyChallenge : ModAccuracyChallenge
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
}
}
@@ -16,7 +16,7 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAutopilot : Mod, IApplicableFailOverride, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
public class OsuModAutopilot : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => "Autopilot";
public override string Acronym => "AP";
@@ -29,18 +29,12 @@ namespace osu.Game.Rulesets.Osu.Mods
{
typeof(OsuModSpunOut),
typeof(ModRelax),
typeof(ModFailCondition),
typeof(ModNoFail),
typeof(ModAutoplay),
typeof(OsuModMagnetised),
typeof(OsuModRepel),
typeof(ModTouchDevice)
};
public bool PerformFail() => false;
public bool RestartOnFail => false;
private OsuInputManager inputManager = null!;
private List<OsuReplayFrame> replayFrames = null!;
+6 -1
View File
@@ -18,7 +18,7 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableHealthProcessor
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModStrictTracking)).ToArray();
@@ -34,6 +34,9 @@ namespace osu.Game.Rulesets.Osu.Mods
[SettingSource("Fade out hit circles earlier", "Make hit circles fade out into a miss, rather than after it.")]
public Bindable<bool> FadeHitCircleEarly { get; } = new Bindable<bool>(true);
[SettingSource("Classic health", "More closely resembles the original HP drain mechanics.")]
public Bindable<bool> ClassicHealth { get; } = new Bindable<bool>(true);
private bool usingHiddenFading;
public void ApplyToHitObject(HitObject hitObject)
@@ -115,5 +118,7 @@ namespace osu.Game.Rulesets.Osu.Mods
}
};
}
public HealthProcessor? CreateHealthProcessor(double drainStartTime) => ClassicHealth.Value ? new OsuLegacyHealthProcessor(drainStartTime) : null;
}
}
@@ -1,14 +1,11 @@
// 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.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModNoFail : ModNoFail
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
}
}
@@ -1,14 +1,11 @@
// 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.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModPerfect : ModPerfect
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray();
}
}
+1 -1
View File
@@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods
if (!slider.HeadCircle.IsHit)
handleHitCircle(slider.HeadCircle);
requiresHold |= slider.Ball.IsHovered || h.IsHovered;
requiresHold |= slider.SliderInputManager.IsMouseInFollowArea(true);
break;
case DrawableSpinner spinner:
@@ -36,6 +36,9 @@ namespace osu.Game.Rulesets.Osu.Mods
{
if (e.NewValue || slider.Judged) return;
if (slider.Time.Current < slider.HitObject.StartTime)
return;
var tail = slider.NestedHitObjects.OfType<StrictTrackingDrawableSliderTail>().First();
if (!tail.Judged)
@@ -11,7 +11,6 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
{
typeof(OsuModAutopilot),
typeof(OsuModTargetPractice),
}).ToArray();
}
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class DrawableHitCircle : DrawableOsuHitObject, IHasApproachCircle
{
public OsuAction? HitAction => HitArea.HitAction;
public OsuAction? HitAction => HitArea?.HitAction;
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
public SkinnableDrawable ApproachCircle { get; private set; }
@@ -97,6 +97,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// </summary>
public virtual void Shake() { }
/// <summary>
/// Causes this <see cref="DrawableOsuHitObject"/> to get hit, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
/// </summary>
public void HitForcefully() => ApplyResult(r => r.Type = r.Judgement.MaxResult);
/// <summary>
/// Causes this <see cref="DrawableOsuHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
/// </summary>
@@ -58,6 +58,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public IBindable<int> PathVersion => pathVersion;
private readonly Bindable<int> pathVersion = new Bindable<int>();
public readonly SliderInputManager SliderInputManager;
private Container<DrawableSliderHead> headContainer;
private Container<DrawableSliderTail> tailContainer;
private Container<DrawableSliderTick> tickContainer;
@@ -72,9 +74,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public DrawableSlider([CanBeNull] Slider s = null)
: base(s)
{
SliderInputManager = new SliderInputManager(this);
Ball = new DrawableSliderBall
{
GetInitialHitAction = () => HeadCircle.HitAction,
BypassAutoSizeAxes = Axes.Both,
AlwaysPresent = true,
Alpha = 0
@@ -88,6 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
AddRangeInternal(new Drawable[]
{
SliderInputManager,
shakeContainer = new ShakeContainer
{
ShakeDuration = 30,
@@ -124,8 +128,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
foreach (var drawableHitObject in NestedHitObjects)
drawableHitObject.AccentColour.Value = colour.NewValue;
}, true);
Tracking.BindValueChanged(updateSlidingSample);
}
protected override void OnApply()
@@ -162,14 +164,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
slidingSample?.Stop();
}
private void updateSlidingSample(ValueChangedEvent<bool> tracking)
{
if (tracking.NewValue)
slidingSample?.Play();
else
slidingSample?.Stop();
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
base.AddNestedHitObject(hitObject);
@@ -232,11 +226,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.Update();
Tracking.Value = Ball.Tracking;
Tracking.Value = SliderInputManager.Tracking;
if (Tracking.Value && slidingSample != null)
// keep the sliding sample playing at the current tracking position
slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball));
if (slidingSample != null)
{
if (Tracking.Value && Time.Current >= HitObject.StartTime)
{
// keep the sliding sample playing at the current tracking position
if (!slidingSample.IsPlaying)
slidingSample.Play();
slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball));
}
else if (slidingSample.IsPlaying)
slidingSample.Stop();
}
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);
@@ -245,8 +248,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
foreach (DrawableHitObject hitObject in NestedHitObjects)
{
if (hitObject is ITrackSnaking s) s.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0));
if (hitObject is IRequireTracking t) t.Tracking = Ball.Tracking;
if (hitObject is ITrackSnaking s)
s.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0));
}
Size = SliderBody?.Size ?? Vector2.Zero;
@@ -4,14 +4,9 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Skinning.Default;
@@ -21,13 +16,10 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition
public partial class DrawableSliderBall : CircularContainer, ISliderProgress
{
public const float FOLLOW_AREA = 2.4f;
public Func<OsuAction?> GetInitialHitAction;
private Drawable followCircleReceptor;
private DrawableSlider drawableSlider;
private Drawable ball;
@@ -48,13 +40,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
},
followCircleReceptor = new CircularContainer
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true
},
ball = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall())
{
Anchor = Anchor.Centre,
@@ -63,14 +48,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
};
}
private Vector2? lastScreenSpaceMousePosition;
protected override bool OnMouseMove(MouseMoveEvent e)
{
lastScreenSpaceMousePosition = e.ScreenSpaceMousePosition;
return base.OnMouseMove(e);
}
public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null)
{
// Consider the case of rewinding - children's transforms are handled internally, so propagating down
@@ -86,102 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.ApplyTransformsAt(time, false);
}
private bool tracking;
public bool Tracking
{
get => tracking;
private set
{
if (value == tracking)
return;
tracking = value;
followCircleReceptor.Scale = new Vector2(tracking ? FOLLOW_AREA : 1f);
}
}
/// <summary>
/// If the cursor moves out of the ball's radius we still need to be able to receive positional updates to stop tracking.
/// </summary>
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
/// <summary>
/// The point in time after which we can accept any key for tracking. Before this time, we may need to restrict tracking to the key used to hit the head circle.
///
/// This is a requirement to stop the case where a player holds down one key (from before the slider) and taps the second key while maintaining full scoring (tracking) of sliders.
/// Visually, this special case can be seen below (time increasing from left to right):
///
/// Z Z+X Z
/// o========o
///
/// Without this logic, tracking would continue through the entire slider even though no key hold action is directly attributing to it.
///
/// In all other cases, no special handling is required (either key being pressed is allowable as valid tracking).
///
/// The reason for storing this as a time value (rather than a bool) is to correctly handle rewind scenarios.
/// </summary>
private double? timeToAcceptAnyKeyAfter;
/// <summary>
/// The actions that were pressed in the previous frame.
/// </summary>
private readonly List<OsuAction> lastPressedActions = new List<OsuAction>();
protected override void Update()
{
base.Update();
// from the point at which the head circle is hit, this will be non-null.
// it may be null if the head circle was missed.
var headCircleHitAction = GetInitialHitAction();
if (headCircleHitAction == null)
timeToAcceptAnyKeyAfter = null;
var actions = drawableSlider.OsuActionInputManager?.PressedActions;
// if the head circle was hit with a specific key, tracking should only occur while that key is pressed.
if (headCircleHitAction != null && timeToAcceptAnyKeyAfter == null)
{
var otherKey = headCircleHitAction == OsuAction.RightButton ? OsuAction.LeftButton : OsuAction.RightButton;
// we can start accepting any key once all other keys have been released in the previous frame.
if (!lastPressedActions.Contains(otherKey))
timeToAcceptAnyKeyAfter = Time.Current;
}
Tracking =
// in valid time range
Time.Current >= drawableSlider.HitObject.StartTime
// even in an edge case where current time has exceeded the slider's time, we may not have finished judging.
// we don't want to potentially update from Tracking=true to Tracking=false at this point.
&& (!drawableSlider.AllJudged || Time.Current <= drawableSlider.HitObject.GetEndTime())
// in valid position range
&& lastScreenSpaceMousePosition.HasValue && followCircleReceptor.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) &&
// valid action
(actions?.Any(isValidTrackingAction) ?? false);
lastPressedActions.Clear();
if (actions != null)
lastPressedActions.AddRange(actions);
}
/// <summary>
/// Check whether a given user input is a valid tracking action.
/// </summary>
private bool isValidTrackingAction(OsuAction action)
{
bool headCircleHit = GetInitialHitAction().HasValue;
// if the head circle was hit, we may not yet be allowed to accept any key, so we must use the initial hit action.
if (headCircleHit && (!timeToAcceptAnyKeyAfter.HasValue || Time.Current <= timeToAcceptAnyKeyAfter.Value))
return action == GetInitialHitAction();
return action == OsuAction.LeftButton || action == OsuAction.RightButton;
}
private Vector2? lastPosition;
public void UpdateProgress(double completionProgress)
@@ -61,6 +61,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
CheckHittable = (d, t, r) => DrawableSlider.CheckHittable?.Invoke(d, t, r) ?? ClickAction.Hit;
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
base.CheckForResult(userTriggered, timeOffset);
DrawableSlider.SliderInputManager.PostProcessHeadJudgement(this);
}
protected override HitResult ResultFor(double timeOffset)
{
Debug.Assert(HitObject != null);
@@ -17,7 +17,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking, IRequireTracking
public partial class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking
{
public new SliderRepeat HitObject => (SliderRepeat)base.HitObject;
@@ -34,10 +34,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private Drawable scaleContainer;
public override bool DisplayResult => false;
public bool Tracking { get; set; }
public DrawableSliderRepeat()
: base(null)
{
@@ -85,21 +81,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Position = HitObject.Position - DrawableSlider.Position;
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
// shared implementation with DrawableSliderTick.
if (timeOffset >= 0)
{
// Attempt to preserve correct ordering of judgements as best we can by forcing
// an un-judged head to be missed when the user has clearly skipped it.
//
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
if (Tracking && !DrawableSlider.HeadCircle.Judged)
DrawableSlider.HeadCircle.MissForcefully();
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
}
protected override void CheckForResult(bool userTriggered, double timeOffset) => DrawableSlider.SliderInputManager.TryJudgeNestedObject(this, timeOffset);
protected override void UpdateInitialTransforms()
{
@@ -4,12 +4,10 @@
#nullable disable
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
@@ -17,7 +15,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking
public partial class DrawableSliderTail : DrawableOsuHitObject
{
public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject;
@@ -26,19 +24,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
/// <summary>
/// The judgement text is provided by the <see cref="DrawableSlider"/>.
/// </summary>
public override bool DisplayResult => false;
/// <summary>
/// Whether the hit samples only play on successful hits.
/// If <c>false</c>, the hit samples will also play on misses.
/// </summary>
public bool SamplePlaysOnlyOnHit { get; set; } = true;
public bool Tracking { get; set; }
public SkinnableDrawable CirclePiece { get; private set; }
private Container scaleContainer;
@@ -125,36 +116,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (userTriggered)
return;
// Ensure the tail can only activate after all previous ticks/repeats already have.
//
// This covers the edge case where the lenience may allow the tail to activate before
// the last tick, changing ordering of score/combo awarding.
var lastTick = DrawableSlider.NestedHitObjects.LastOrDefault(o => o.HitObject is SliderTick || o.HitObject is SliderRepeat);
if (lastTick?.Judged == false)
return;
if (timeOffset < SliderEventGenerator.TAIL_LENIENCY)
return;
// Attempt to preserve correct ordering of judgements as best we can by forcing
// an un-judged head to be missed when the user has clearly skipped it.
//
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
if (Tracking && !DrawableSlider.HeadCircle.Judged)
DrawableSlider.HeadCircle.MissForcefully();
// The player needs to have engaged in tracking at any point after the tail leniency cutoff.
// An actual tick miss should only occur if reaching the tick itself.
if (Tracking)
ApplyResult(r => r.Type = r.Judgement.MaxResult);
else if (timeOffset > 0)
ApplyResult(r => r.Type = r.Judgement.MinResult);
}
protected override void CheckForResult(bool userTriggered, double timeOffset) => DrawableSlider.SliderInputManager.TryJudgeNestedObject(this, timeOffset);
protected override void OnApply()
{
@@ -14,16 +14,12 @@ using osu.Framework.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class DrawableSliderTick : DrawableOsuHitObject, IRequireTracking
public partial class DrawableSliderTick : DrawableOsuHitObject
{
public const double ANIM_DURATION = 150;
private const float default_tick_size = 16;
public bool Tracking { get; set; }
public override bool DisplayResult => false;
protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
private SkinnableDrawable scaleContainer;
@@ -73,21 +69,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Position = HitObject.Position - DrawableSlider.HitObject.Position;
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
// shared implementation with DrawableSliderRepeat.
if (timeOffset >= 0)
{
// Attempt to preserve correct ordering of judgements as best we can by forcing
// an un-judged head to be missed when the user has clearly skipped it.
//
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
if (Tracking && !DrawableSlider.HeadCircle.Judged)
DrawableSlider.HeadCircle.MissForcefully();
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
}
protected override void CheckForResult(bool userTriggered, double timeOffset) => DrawableSlider.SliderInputManager.TryJudgeNestedObject(this, timeOffset);
protected override void UpdateInitialTransforms()
{
@@ -107,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
case ArmedState.Miss:
this.FadeOut(ANIM_DURATION);
this.FadeColour(Color4.Red, ANIM_DURATION / 2);
this.TransformBindableTo(AccentColour, Color4.Red, 0);
break;
case ArmedState.Hit:
@@ -17,6 +17,7 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Scoring;
@@ -312,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
updateBonusScore();
}
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
private static readonly int score_per_tick = new OsuScoreProcessor().GetBaseScoreForResult(new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxResult);
private void updateBonusScore()
{
@@ -1,13 +0,0 @@
// 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.
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public interface IRequireTracking
{
/// <summary>
/// Whether the <see cref="DrawableSlider"/> is currently being tracked by the user.
/// </summary>
bool Tracking { get; set; }
}
}
@@ -0,0 +1,260 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class SliderInputManager : Component, IRequireHighFrequencyMousePosition
{
/// <summary>
/// Whether the slider is currently being tracked.
/// </summary>
public bool Tracking { get; private set; }
/// <summary>
/// The point in time after which we can accept any key for tracking. Before this time, we may need to restrict tracking to the key used to hit the head circle.
///
/// This is a requirement to stop the case where a player holds down one key (from before the slider) and taps the second key while maintaining full scoring (tracking) of sliders.
/// Visually, this special case can be seen below (time increasing from left to right):
///
/// Z Z+X Z
/// o========o
///
/// Without this logic, tracking would continue through the entire slider even though no key hold action is directly attributing to it.
///
/// In all other cases, no special handling is required (either key being pressed is allowable as valid tracking).
///
/// The reason for storing this as a time value (rather than a bool) is to correctly handle rewind scenarios.
/// </summary>
private double? timeToAcceptAnyKeyAfter;
/// <summary>
/// The actions that were pressed in the previous frame.
/// </summary>
private readonly List<OsuAction> lastPressedActions = new List<OsuAction>();
private Vector2? screenSpaceMousePosition;
private readonly DrawableSlider slider;
public SliderInputManager(DrawableSlider slider)
{
this.slider = slider;
}
/// <summary>
/// This component handles all input of the slider, so it should receive input no matter the position.
/// </summary>
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
protected override bool OnMouseMove(MouseMoveEvent e)
{
screenSpaceMousePosition = e.ScreenSpaceMousePosition;
return base.OnMouseMove(e);
}
protected override void Update()
{
base.Update();
updateTracking(IsMouseInFollowArea(Tracking));
}
public void PostProcessHeadJudgement(DrawableSliderHead head)
{
if (!head.Judged || !head.Result.IsHit)
return;
if (!IsMouseInFollowArea(true))
return;
Debug.Assert(screenSpaceMousePosition != null);
Vector2 mousePositionInSlider = slider.ToLocalSpace(screenSpaceMousePosition.Value) - slider.OriginPosition;
// When the head is hit late:
// - If the cursor has at all times been within range of the expanded follow area, hit all nested objects that have been passed through.
// - If the cursor has at some point left the expanded follow area, miss those nested objects instead.
bool allTicksInRange = true;
foreach (var nested in slider.NestedHitObjects.OfType<DrawableOsuHitObject>())
{
// Skip nested objects that are already judged.
if (nested.Judged)
continue;
// Stop the process when a nested object is reached that can't be hit before the current time.
if (nested.HitObject.StartTime > Time.Current)
break;
float radius = getFollowRadius(true);
double objectProgress = Math.Clamp((nested.HitObject.StartTime - slider.HitObject.StartTime) / slider.HitObject.Duration, 0, 1);
Vector2 objectPosition = slider.HitObject.CurvePositionAt(objectProgress);
// When the first nested object that is further outside the follow area is reached,
// forcefully miss all other nested objects that would otherwise be valid to be hit.
// This covers a case of a slider overlapping itself that requires tracking to a tick on an outer edge.
if ((objectPosition - mousePositionInSlider).LengthSquared > radius * radius)
{
allTicksInRange = false;
break;
}
}
foreach (var nested in slider.NestedHitObjects.OfType<DrawableOsuHitObject>())
{
// Skip nested objects that are already judged.
if (nested.Judged)
continue;
// Stop the process when a nested object is reached that can't be hit before the current time.
if (nested.HitObject.StartTime > Time.Current)
break;
if (allTicksInRange)
nested.HitForcefully();
else
nested.MissForcefully();
}
// If all ticks were hit so far, enable tracking the full extent.
// If any ticks were missed, assume tracking would've broken at some point, and should only activate if the cursor is within the slider ball.
// For the second case, this may be the last chance we have to enable tracking before other objects get judged, otherwise the same would normally happen via Update().
updateTracking(allTicksInRange || IsMouseInFollowArea(false));
}
public void TryJudgeNestedObject(DrawableOsuHitObject nestedObject, double timeOffset)
{
switch (nestedObject)
{
case DrawableSliderRepeat:
case DrawableSliderTick:
if (timeOffset < 0)
return;
break;
case DrawableSliderTail:
if (timeOffset < SliderEventGenerator.TAIL_LENIENCY)
return;
// Ensure the tail can only activate after all previous ticks/repeats already have.
//
// This covers the edge case where the lenience may allow the tail to activate before
// the last tick, changing ordering of score/combo awarding.
var lastTick = slider.NestedHitObjects.LastOrDefault(o => o.HitObject is SliderTick || o.HitObject is SliderRepeat);
if (lastTick?.Judged == false)
return;
break;
default:
return;
}
if (!slider.HeadCircle.Judged)
return;
if (Tracking)
nestedObject.HitForcefully();
else if (timeOffset >= 0)
nestedObject.MissForcefully();
}
/// <summary>
/// Whether the mouse is currently in the follow area.
/// </summary>
/// <param name="expanded">Whether to test against the maximum area of the follow circle.</param>
public bool IsMouseInFollowArea(bool expanded)
{
if (screenSpaceMousePosition is not Vector2 pos)
return false;
float radius = getFollowRadius(expanded);
double followProgress = Math.Clamp((Time.Current - slider.HitObject.StartTime) / slider.HitObject.Duration, 0, 1);
Vector2 followCirclePosition = slider.HitObject.CurvePositionAt(followProgress);
Vector2 mousePositionInSlider = slider.ToLocalSpace(pos) - slider.OriginPosition;
return (mousePositionInSlider - followCirclePosition).LengthSquared <= radius * radius;
}
/// <summary>
/// Retrieves the radius of the follow area.
/// </summary>
/// <param name="expanded">Whether to return the maximum area of the follow circle.</param>
private float getFollowRadius(bool expanded)
{
float radius = (float)slider.HitObject.Radius;
if (expanded)
radius *= DrawableSliderBall.FOLLOW_AREA;
return radius;
}
/// <summary>
/// Updates the tracking state.
/// </summary>
/// <param name="isValidTrackingPosition">Whether the current mouse position is valid to begin tracking.</param>
private void updateTracking(bool isValidTrackingPosition)
{
// from the point at which the head circle is hit, this will be non-null.
// it may be null if the head circle was missed.
OsuAction? headCircleHitAction = getInitialHitAction();
if (headCircleHitAction == null)
timeToAcceptAnyKeyAfter = null;
var actions = slider.OsuActionInputManager?.PressedActions;
// if the head circle was hit with a specific key, tracking should only occur while that key is pressed.
if (headCircleHitAction != null && timeToAcceptAnyKeyAfter == null)
{
var otherKey = headCircleHitAction == OsuAction.RightButton ? OsuAction.LeftButton : OsuAction.RightButton;
// we can start accepting any key once all other keys have been released in the previous frame.
if (!lastPressedActions.Contains(otherKey))
timeToAcceptAnyKeyAfter = Time.Current;
}
Tracking =
// even in an edge case where current time has exceeded the slider's time, we may not have finished judging.
// we don't want to potentially update from Tracking=true to Tracking=false at this point.
(!slider.AllJudged || Time.Current <= slider.HitObject.GetEndTime())
// in valid position range
&& isValidTrackingPosition
// valid action
&& (actions?.Any(isValidTrackingAction) ?? false);
lastPressedActions.Clear();
if (actions != null)
lastPressedActions.AddRange(actions);
}
private OsuAction? getInitialHitAction() => slider.HeadCircle?.HitAction;
/// <summary>
/// Check whether a given user input is a valid tracking action.
/// </summary>
private bool isValidTrackingAction(OsuAction action)
{
OsuAction? hitAction = getInitialHitAction();
// if the head circle was hit, we may not yet be allowed to accept any key, so we must use the initial hit action.
if (hitAction.HasValue && (!timeToAcceptAnyKeyAfter.HasValue || Time.Current <= timeToAcceptAnyKeyAfter.Value))
return action == hitAction;
return action == OsuAction.LeftButton || action == OsuAction.RightButton;
}
}
}
+11 -1
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.
+18
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;
}
}
}

Some files were not shown because too many files have changed in this diff Show More